mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-14 02:10:07 +08:00
feat: add work items CLI
This commit is contained in:
parent
8926ea925e
commit
b1e67788f7
@ -94,7 +94,7 @@ This repo is the raw code only. The guides explain everything.
|
|||||||
- **Media and launch tooling** — `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system.
|
- **Media and launch tooling** — `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system.
|
||||||
- **Framework and product surface growth** — `nestjs-patterns`, richer Codex/OpenCode install surfaces, and expanded cross-harness packaging keep the repo usable beyond Claude Code alone.
|
- **Framework and product surface growth** — `nestjs-patterns`, richer Codex/OpenCode install surfaces, and expanded cross-harness packaging keep the repo usable beyond Claude Code alone.
|
||||||
- **ECC 2.0 alpha is in-tree** — the Rust control-plane prototype in `ecc2/` now builds locally and exposes `dashboard`, `start`, `sessions`, `status`, `stop`, `resume`, and `daemon` commands. It is usable as an alpha, not yet a general release.
|
- **ECC 2.0 alpha is in-tree** — the Rust control-plane prototype in `ecc2/` now builds locally and exposes `dashboard`, `start`, `sessions`, `status`, `stop`, `resume`, and `daemon` commands. It is usable as an alpha, not yet a general release.
|
||||||
- **Operator status snapshots** — `ecc status --markdown --write status.md` turns the local state store into a portable handoff covering readiness, active sessions, skill-run health, install health, pending governance events, and linked work items from Linear/GitHub/handoffs.
|
- **Operator status snapshots** — `ecc status --markdown --write status.md` turns the local state store into a portable handoff covering readiness, active sessions, skill-run health, install health, pending governance events, and linked work items from Linear/GitHub/handoffs. Use `ecc work-items upsert ...` to add or update those linked work items from the CLI.
|
||||||
- **Ecosystem hardening** — AgentShield, ECC Tools cost controls, billing portal work, and website refreshes continue to ship around the core plugin instead of drifting into separate silos.
|
- **Ecosystem hardening** — AgentShield, ECC Tools cost controls, billing portal work, and website refreshes continue to ship around the core plugin instead of drifting into separate silos.
|
||||||
|
|
||||||
### v1.9.0 — Selective Install & Language Expansion (Mar 2026)
|
### v1.9.0 — Selective Install & Language Expansion (Mar 2026)
|
||||||
|
|||||||
@ -87,6 +87,7 @@
|
|||||||
"scripts/setup-package-manager.js",
|
"scripts/setup-package-manager.js",
|
||||||
"scripts/skill-create-output.js",
|
"scripts/skill-create-output.js",
|
||||||
"scripts/status.js",
|
"scripts/status.js",
|
||||||
|
"scripts/work-items.js",
|
||||||
"scripts/uninstall.js",
|
"scripts/uninstall.js",
|
||||||
"skills/agent-harness-construction/",
|
"skills/agent-harness-construction/",
|
||||||
"skills/agent-introspection-debugging/",
|
"skills/agent-introspection-debugging/",
|
||||||
|
|||||||
@ -49,6 +49,10 @@ const COMMANDS = {
|
|||||||
script: 'sessions-cli.js',
|
script: 'sessions-cli.js',
|
||||||
description: 'List or inspect ECC sessions from the SQLite state store',
|
description: 'List or inspect ECC sessions from the SQLite state store',
|
||||||
},
|
},
|
||||||
|
'work-items': {
|
||||||
|
script: 'work-items.js',
|
||||||
|
description: 'Track linked Linear, GitHub, handoff, and manual work items',
|
||||||
|
},
|
||||||
'session-inspect': {
|
'session-inspect': {
|
||||||
script: 'session-inspect.js',
|
script: 'session-inspect.js',
|
||||||
description: 'Emit canonical ECC session snapshots from dmux or Claude history targets',
|
description: 'Emit canonical ECC session snapshots from dmux or Claude history targets',
|
||||||
@ -74,6 +78,7 @@ const PRIMARY_COMMANDS = [
|
|||||||
'auto-update',
|
'auto-update',
|
||||||
'status',
|
'status',
|
||||||
'sessions',
|
'sessions',
|
||||||
|
'work-items',
|
||||||
'session-inspect',
|
'session-inspect',
|
||||||
'loop-status',
|
'loop-status',
|
||||||
'uninstall',
|
'uninstall',
|
||||||
@ -111,6 +116,7 @@ Examples:
|
|||||||
ecc status --markdown --write status.md
|
ecc status --markdown --write status.md
|
||||||
ecc sessions
|
ecc sessions
|
||||||
ecc sessions session-active --json
|
ecc sessions session-active --json
|
||||||
|
ecc work-items upsert linear-ecc-20 --source linear --source-id ECC-20 --title "Review control-plane contract" --status blocked
|
||||||
ecc session-inspect claude:latest
|
ecc session-inspect claude:latest
|
||||||
ecc loop-status --json
|
ecc loop-status --json
|
||||||
ecc uninstall --target antigravity --dry-run
|
ecc uninstall --target antigravity --dry-run
|
||||||
|
|||||||
@ -450,6 +450,10 @@ function createQueryApi(db) {
|
|||||||
ORDER BY updated_at DESC, id DESC
|
ORDER BY updated_at DESC, id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`);
|
`);
|
||||||
|
const countWorkItemsStatement = db.prepare(`
|
||||||
|
SELECT COUNT(*) AS total_count
|
||||||
|
FROM work_items
|
||||||
|
`);
|
||||||
const listAllWorkItemsStatement = db.prepare(`
|
const listAllWorkItemsStatement = db.prepare(`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM work_items
|
FROM work_items
|
||||||
@ -690,6 +694,11 @@ function createQueryApi(db) {
|
|||||||
return row ? mapSessionRow(row) : null;
|
return row ? mapSessionRow(row) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWorkItemById(id) {
|
||||||
|
const row = getWorkItemStatement.get(id);
|
||||||
|
return row ? mapWorkItemRow(row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
function listRecentSessions(options = {}) {
|
function listRecentSessions(options = {}) {
|
||||||
const limit = normalizeLimit(options.limit, 10);
|
const limit = normalizeLimit(options.limit, 10);
|
||||||
return {
|
return {
|
||||||
@ -716,6 +725,14 @@ function createQueryApi(db) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listWorkItems(options = {}) {
|
||||||
|
const limit = normalizeLimit(options.limit, 20);
|
||||||
|
return {
|
||||||
|
totalCount: countWorkItemsStatement.get().total_count,
|
||||||
|
items: listWorkItemsStatement.all(limit).map(mapWorkItemRow),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getStatus(options = {}) {
|
function getStatus(options = {}) {
|
||||||
const activeLimit = normalizeLimit(options.activeLimit, 5);
|
const activeLimit = normalizeLimit(options.activeLimit, 5);
|
||||||
const recentSkillRunLimit = normalizeLimit(options.recentSkillRunLimit, 20);
|
const recentSkillRunLimit = normalizeLimit(options.recentSkillRunLimit, 20);
|
||||||
@ -763,6 +780,7 @@ function createQueryApi(db) {
|
|||||||
return {
|
return {
|
||||||
getSessionById,
|
getSessionById,
|
||||||
getSessionDetail,
|
getSessionDetail,
|
||||||
|
getWorkItemById,
|
||||||
getStatus,
|
getStatus,
|
||||||
insertDecision(decision) {
|
insertDecision(decision) {
|
||||||
const normalized = normalizeDecisionInput(decision);
|
const normalized = normalizeDecisionInput(decision);
|
||||||
@ -812,6 +830,7 @@ function createQueryApi(db) {
|
|||||||
return normalized;
|
return normalized;
|
||||||
},
|
},
|
||||||
listRecentSessions,
|
listRecentSessions,
|
||||||
|
listWorkItems,
|
||||||
upsertInstallState(installState) {
|
upsertInstallState(installState) {
|
||||||
const normalized = normalizeInstallStateInput(installState);
|
const normalized = normalizeInstallStateInput(installState);
|
||||||
assertValidEntity('installState', normalized);
|
assertValidEntity('installState', normalized);
|
||||||
|
|||||||
291
scripts/work-items.js
Normal file
291
scripts/work-items.js
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const os = require('os');
|
||||||
|
const { createStateStore } = require('./lib/state-store');
|
||||||
|
|
||||||
|
const VALUE_FLAGS = new Set([
|
||||||
|
'--db',
|
||||||
|
'--id',
|
||||||
|
'--limit',
|
||||||
|
'--metadata-json',
|
||||||
|
'--owner',
|
||||||
|
'--priority',
|
||||||
|
'--repo',
|
||||||
|
'--repo-root',
|
||||||
|
'--session',
|
||||||
|
'--session-id',
|
||||||
|
'--source',
|
||||||
|
'--source-id',
|
||||||
|
'--status',
|
||||||
|
'--title',
|
||||||
|
'--url',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function showHelp(exitCode = 0) {
|
||||||
|
console.log(`
|
||||||
|
Usage:
|
||||||
|
node scripts/work-items.js list [--db <path>] [--json] [--limit <n>]
|
||||||
|
node scripts/work-items.js show <id> [--db <path>] [--json]
|
||||||
|
node scripts/work-items.js upsert [<id>] --title <title> [options] [--json]
|
||||||
|
node scripts/work-items.js close <id> [--status done] [--db <path>] [--json]
|
||||||
|
|
||||||
|
Track Linear, GitHub, handoff, and manual roadmap items in the ECC SQLite state
|
||||||
|
store so "ecc status" can include linked work and blocked operator follow-up.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--id <id> Stable local work-item id for upsert
|
||||||
|
--source <source> Source system, e.g. linear, github, handoff, manual
|
||||||
|
--source-id <id> Source-local identifier, e.g. ECC-20 or PR number
|
||||||
|
--status <status> Status such as open, in-progress, blocked, done
|
||||||
|
--priority <priority> Optional priority label
|
||||||
|
--url <url> Optional source URL
|
||||||
|
--owner <owner> Optional owner label
|
||||||
|
--repo-root <path> Optional repo root to associate with this item
|
||||||
|
--repo <path> Alias for --repo-root
|
||||||
|
--session-id <id> Optional ECC session id
|
||||||
|
--session <id> Alias for --session-id
|
||||||
|
--metadata-json <json> Optional JSON metadata payload
|
||||||
|
--db <path> SQLite state database path
|
||||||
|
--json Emit JSON
|
||||||
|
`);
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignOption(options, flag, value) {
|
||||||
|
if (flag === '--db') options.dbPath = value;
|
||||||
|
else if (flag === '--id') options.id = value;
|
||||||
|
else if (flag === '--limit') options.limit = value;
|
||||||
|
else if (flag === '--metadata-json') options.metadataJson = value;
|
||||||
|
else if (flag === '--owner') options.owner = value;
|
||||||
|
else if (flag === '--priority') options.priority = value;
|
||||||
|
else if (flag === '--repo' || flag === '--repo-root') options.repoRoot = value;
|
||||||
|
else if (flag === '--session' || flag === '--session-id') options.sessionId = value;
|
||||||
|
else if (flag === '--source') options.source = value;
|
||||||
|
else if (flag === '--source-id') options.sourceId = value;
|
||||||
|
else if (flag === '--status') options.status = value;
|
||||||
|
else if (flag === '--title') options.title = value;
|
||||||
|
else if (flag === '--url') options.url = value;
|
||||||
|
else throw new Error(`Unknown argument: ${flag}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = argv.slice(2);
|
||||||
|
const parsed = {
|
||||||
|
command: 'list',
|
||||||
|
dbPath: null,
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
limit: 20,
|
||||||
|
positionals: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (args[0] && !args[0].startsWith('-')) {
|
||||||
|
parsed.command = args.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
|
const arg = args[index];
|
||||||
|
if (arg === '--help' || arg === '-h') {
|
||||||
|
parsed.help = true;
|
||||||
|
} else if (arg === '--json') {
|
||||||
|
parsed.json = true;
|
||||||
|
} else if (VALUE_FLAGS.has(arg)) {
|
||||||
|
const value = args[index + 1];
|
||||||
|
if (!value || value.startsWith('--')) {
|
||||||
|
throw new Error(`Missing value for ${arg}`);
|
||||||
|
}
|
||||||
|
assignOption(parsed, arg, value);
|
||||||
|
index += 1;
|
||||||
|
} else if (!arg.startsWith('-')) {
|
||||||
|
parsed.positionals.push(arg);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMetadataJson(value) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Invalid --metadata-json: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWorkItemId(options) {
|
||||||
|
return options.id || options.positionals[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLimit(value) {
|
||||||
|
const parsed = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
|
throw new Error(`Invalid limit: ${value}`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUpsertPayload(options, existing = null) {
|
||||||
|
const id = resolveWorkItemId(options);
|
||||||
|
if (!id) {
|
||||||
|
throw new Error('Missing work item id. Pass <id> or --id <id>.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = options.title ?? (existing && existing.title);
|
||||||
|
if (!title) {
|
||||||
|
throw new Error('Missing --title for a new work item.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
source: options.source ?? (existing && existing.source) ?? 'manual',
|
||||||
|
sourceId: options.sourceId ?? (existing && existing.sourceId) ?? null,
|
||||||
|
title,
|
||||||
|
status: options.status ?? (existing && existing.status) ?? 'open',
|
||||||
|
priority: options.priority ?? (existing && existing.priority) ?? null,
|
||||||
|
url: options.url ?? (existing && existing.url) ?? null,
|
||||||
|
owner: options.owner ?? (existing && existing.owner) ?? null,
|
||||||
|
repoRoot: options.repoRoot ?? (existing && existing.repoRoot) ?? process.cwd(),
|
||||||
|
sessionId: options.sessionId ?? (existing && existing.sessionId) ?? null,
|
||||||
|
metadata: options.metadataJson !== undefined
|
||||||
|
? parseMetadataJson(options.metadataJson)
|
||||||
|
: ((existing && existing.metadata) ?? null),
|
||||||
|
createdAt: existing ? existing.createdAt : undefined,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function printWorkItem(item) {
|
||||||
|
const sourceId = item.sourceId ? `#${item.sourceId}` : item.id;
|
||||||
|
console.log(`${item.source}/${sourceId} ${item.status}: ${item.title}`);
|
||||||
|
console.log(`ID: ${item.id}`);
|
||||||
|
console.log(`Priority: ${item.priority || '(none)'}`);
|
||||||
|
console.log(`Owner: ${item.owner || '(unassigned)'}`);
|
||||||
|
console.log(`Repo: ${item.repoRoot || '(none)'}`);
|
||||||
|
console.log(`Session: ${item.sessionId || '(none)'}`);
|
||||||
|
console.log(`Updated: ${item.updatedAt}`);
|
||||||
|
if (item.url) {
|
||||||
|
console.log(`URL: ${item.url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printWorkItemList(payload) {
|
||||||
|
console.log(`Work items: ${payload.items.length} shown / ${payload.totalCount} total`);
|
||||||
|
if (payload.items.length === 0) {
|
||||||
|
console.log(' - none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of payload.items) {
|
||||||
|
const sourceId = item.sourceId ? `#${item.sourceId}` : item.id;
|
||||||
|
console.log(` - ${item.source}/${sourceId} ${item.status}: ${item.title}`);
|
||||||
|
console.log(` ID: ${item.id}`);
|
||||||
|
console.log(` Owner: ${item.owner || '(unassigned)'}`);
|
||||||
|
console.log(` Updated: ${item.updatedAt}`);
|
||||||
|
if (item.url) {
|
||||||
|
console.log(` URL: ${item.url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let store = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = parseArgs(process.argv);
|
||||||
|
if (options.help) {
|
||||||
|
showHelp(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
store = await createStateStore({
|
||||||
|
dbPath: options.dbPath,
|
||||||
|
homeDir: process.env.HOME || os.homedir(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.command === 'list') {
|
||||||
|
const payload = store.listWorkItems({ limit: normalizeLimit(options.limit) });
|
||||||
|
if (options.json) {
|
||||||
|
console.log(JSON.stringify(payload, null, 2));
|
||||||
|
} else {
|
||||||
|
printWorkItemList(payload);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.command === 'show') {
|
||||||
|
const id = resolveWorkItemId(options);
|
||||||
|
if (!id) {
|
||||||
|
throw new Error('Missing work item id.');
|
||||||
|
}
|
||||||
|
const item = store.getWorkItemById(id);
|
||||||
|
if (!item) {
|
||||||
|
throw new Error(`Work item not found: ${id}`);
|
||||||
|
}
|
||||||
|
if (options.json) {
|
||||||
|
console.log(JSON.stringify(item, null, 2));
|
||||||
|
} else {
|
||||||
|
printWorkItem(item);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.command === 'upsert') {
|
||||||
|
const id = resolveWorkItemId(options);
|
||||||
|
const existing = id ? store.getWorkItemById(id) : null;
|
||||||
|
const item = store.upsertWorkItem(buildUpsertPayload(options, existing));
|
||||||
|
if (options.json) {
|
||||||
|
console.log(JSON.stringify(item, null, 2));
|
||||||
|
} else {
|
||||||
|
printWorkItem(item);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.command === 'close') {
|
||||||
|
const id = resolveWorkItemId(options);
|
||||||
|
if (!id) {
|
||||||
|
throw new Error('Missing work item id.');
|
||||||
|
}
|
||||||
|
const existing = store.getWorkItemById(id);
|
||||||
|
if (!existing) {
|
||||||
|
throw new Error(`Work item not found: ${id}`);
|
||||||
|
}
|
||||||
|
const item = store.upsertWorkItem(buildUpsertPayload({
|
||||||
|
...options,
|
||||||
|
id,
|
||||||
|
status: options.status || 'done',
|
||||||
|
}, existing));
|
||||||
|
if (options.json) {
|
||||||
|
console.log(JSON.stringify(item, null, 2));
|
||||||
|
} else {
|
||||||
|
printWorkItem(item);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown command: ${options.command}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
if (store) {
|
||||||
|
store.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildUpsertPayload,
|
||||||
|
main,
|
||||||
|
parseArgs,
|
||||||
|
};
|
||||||
@ -16,6 +16,7 @@ const {
|
|||||||
const ECC_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'ecc.js');
|
const ECC_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'ecc.js');
|
||||||
const STATUS_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'status.js');
|
const STATUS_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'status.js');
|
||||||
const SESSIONS_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'sessions-cli.js');
|
const SESSIONS_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'sessions-cli.js');
|
||||||
|
const WORK_ITEMS_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'work-items.js');
|
||||||
|
|
||||||
async function test(name, fn) {
|
async function test(name, fn) {
|
||||||
try {
|
try {
|
||||||
@ -697,6 +698,76 @@ async function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed += 1; else failed += 1;
|
})) passed += 1; else failed += 1;
|
||||||
|
|
||||||
|
if (await test('work-items CLI supports upsert, list, show, and close', async () => {
|
||||||
|
const testDir = createTempDir('ecc-work-items-cli-');
|
||||||
|
const dbPath = path.join(testDir, 'state.db');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const upsertResult = runNode(WORK_ITEMS_SCRIPT, [
|
||||||
|
'upsert',
|
||||||
|
'linear-ecc-99',
|
||||||
|
'--db',
|
||||||
|
dbPath,
|
||||||
|
'--source',
|
||||||
|
'linear',
|
||||||
|
'--source-id',
|
||||||
|
'ECC-99',
|
||||||
|
'--title',
|
||||||
|
'Ship work item CLI',
|
||||||
|
'--status',
|
||||||
|
'blocked',
|
||||||
|
'--priority',
|
||||||
|
'high',
|
||||||
|
'--url',
|
||||||
|
'https://linear.app/example/issue/ECC-99',
|
||||||
|
'--owner',
|
||||||
|
'control-plane',
|
||||||
|
'--metadata-json',
|
||||||
|
'{"project":"ECC 2.0"}',
|
||||||
|
'--json',
|
||||||
|
], { cwd: testDir });
|
||||||
|
assert.strictEqual(upsertResult.status, 0, upsertResult.stderr);
|
||||||
|
const upsertPayload = parseJson(upsertResult.stdout);
|
||||||
|
assert.strictEqual(upsertPayload.id, 'linear-ecc-99');
|
||||||
|
assert.strictEqual(upsertPayload.status, 'blocked');
|
||||||
|
assert.strictEqual(upsertPayload.repoRoot, fs.realpathSync(testDir));
|
||||||
|
assert.strictEqual(upsertPayload.metadata.project, 'ECC 2.0');
|
||||||
|
|
||||||
|
const updateResult = runNode(WORK_ITEMS_SCRIPT, [
|
||||||
|
'upsert',
|
||||||
|
'linear-ecc-99',
|
||||||
|
'--db',
|
||||||
|
dbPath,
|
||||||
|
'--status',
|
||||||
|
'in-progress',
|
||||||
|
'--json',
|
||||||
|
]);
|
||||||
|
assert.strictEqual(updateResult.status, 0, updateResult.stderr);
|
||||||
|
const updatePayload = parseJson(updateResult.stdout);
|
||||||
|
assert.strictEqual(updatePayload.title, 'Ship work item CLI');
|
||||||
|
assert.strictEqual(updatePayload.source, 'linear');
|
||||||
|
assert.strictEqual(updatePayload.status, 'in-progress');
|
||||||
|
|
||||||
|
const listResult = runNode(WORK_ITEMS_SCRIPT, ['list', '--db', dbPath, '--json']);
|
||||||
|
assert.strictEqual(listResult.status, 0, listResult.stderr);
|
||||||
|
const listPayload = parseJson(listResult.stdout);
|
||||||
|
assert.strictEqual(listPayload.totalCount, 1);
|
||||||
|
assert.strictEqual(listPayload.items[0].id, 'linear-ecc-99');
|
||||||
|
|
||||||
|
const showResult = runNode(WORK_ITEMS_SCRIPT, ['show', 'linear-ecc-99', '--db', dbPath]);
|
||||||
|
assert.strictEqual(showResult.status, 0, showResult.stderr);
|
||||||
|
assert.match(showResult.stdout, /linear\/#ECC-99 in-progress: Ship work item CLI/);
|
||||||
|
|
||||||
|
const closeResult = runNode(WORK_ITEMS_SCRIPT, ['close', 'linear-ecc-99', '--db', dbPath, '--json']);
|
||||||
|
assert.strictEqual(closeResult.status, 0, closeResult.stderr);
|
||||||
|
const closePayload = parseJson(closeResult.stdout);
|
||||||
|
assert.strictEqual(closePayload.status, 'done');
|
||||||
|
assert.strictEqual(closePayload.title, 'Ship work item CLI');
|
||||||
|
} finally {
|
||||||
|
cleanupTempDir(testDir);
|
||||||
|
}
|
||||||
|
})) passed += 1; else failed += 1;
|
||||||
|
|
||||||
if (await test('sessions CLI supports list and detail views in human-readable and --json output', async () => {
|
if (await test('sessions CLI supports list and detail views in human-readable and --json output', async () => {
|
||||||
const testDir = createTempDir('ecc-state-cli-');
|
const testDir = createTempDir('ecc-state-cli-');
|
||||||
const dbPath = path.join(testDir, 'state.db');
|
const dbPath = path.join(testDir, 'state.db');
|
||||||
@ -729,7 +800,7 @@ async function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed += 1; else failed += 1;
|
})) passed += 1; else failed += 1;
|
||||||
|
|
||||||
if (await test('ecc CLI delegates the new status and sessions subcommands', async () => {
|
if (await test('ecc CLI delegates the new status, sessions, and work-items subcommands', async () => {
|
||||||
const testDir = createTempDir('ecc-state-cli-');
|
const testDir = createTempDir('ecc-state-cli-');
|
||||||
const dbPath = path.join(testDir, 'state.db');
|
const dbPath = path.join(testDir, 'state.db');
|
||||||
|
|
||||||
@ -746,6 +817,29 @@ async function runTests() {
|
|||||||
const sessionsPayload = parseJson(sessionsResult.stdout);
|
const sessionsPayload = parseJson(sessionsResult.stdout);
|
||||||
assert.strictEqual(sessionsPayload.session.id, 'session-active');
|
assert.strictEqual(sessionsPayload.session.id, 'session-active');
|
||||||
assert.strictEqual(sessionsPayload.skillRuns.length, 2);
|
assert.strictEqual(sessionsPayload.skillRuns.length, 2);
|
||||||
|
|
||||||
|
const workItemResult = runNode(ECC_SCRIPT, [
|
||||||
|
'work-items',
|
||||||
|
'upsert',
|
||||||
|
'handoff-roadmap',
|
||||||
|
'--db',
|
||||||
|
dbPath,
|
||||||
|
'--source',
|
||||||
|
'handoff',
|
||||||
|
'--title',
|
||||||
|
'Track roadmap handoff',
|
||||||
|
'--status',
|
||||||
|
'blocked',
|
||||||
|
'--json',
|
||||||
|
], { cwd: testDir });
|
||||||
|
assert.strictEqual(workItemResult.status, 0, workItemResult.stderr);
|
||||||
|
const workItemPayload = parseJson(workItemResult.stdout);
|
||||||
|
assert.strictEqual(workItemPayload.id, 'handoff-roadmap');
|
||||||
|
|
||||||
|
const delegatedStatusResult = runNode(ECC_SCRIPT, ['status', '--db', dbPath, '--json']);
|
||||||
|
assert.strictEqual(delegatedStatusResult.status, 0, delegatedStatusResult.stderr);
|
||||||
|
const delegatedStatusPayload = parseJson(delegatedStatusResult.stdout);
|
||||||
|
assert.strictEqual(delegatedStatusPayload.readiness.blockedWorkItems, 1);
|
||||||
} finally {
|
} finally {
|
||||||
cleanupTempDir(testDir);
|
cleanupTempDir(testDir);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,6 +71,7 @@ function main() {
|
|||||||
assert.match(result.stdout, /auto-update/);
|
assert.match(result.stdout, /auto-update/);
|
||||||
assert.match(result.stdout, /consult/);
|
assert.match(result.stdout, /consult/);
|
||||||
assert.match(result.stdout, /loop-status/);
|
assert.match(result.stdout, /loop-status/);
|
||||||
|
assert.match(result.stdout, /work-items/);
|
||||||
}],
|
}],
|
||||||
['delegates explicit install command', () => {
|
['delegates explicit install command', () => {
|
||||||
const result = runCli(['install', '--dry-run', '--json', 'typescript']);
|
const result = runCli(['install', '--dry-run', '--json', 'typescript']);
|
||||||
@ -201,6 +202,11 @@ function main() {
|
|||||||
assert.strictEqual(result.status, 0, result.stderr);
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
assert.match(result.stdout, /node scripts\/consult\.js "security reviews"/);
|
assert.match(result.stdout, /node scripts\/consult\.js "security reviews"/);
|
||||||
}],
|
}],
|
||||||
|
['supports help for the work-items subcommand', () => {
|
||||||
|
const result = runCli(['help', 'work-items']);
|
||||||
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
|
assert.match(result.stdout, /node scripts\/work-items\.js upsert/);
|
||||||
|
}],
|
||||||
['fails on unknown commands instead of treating them as installs', () => {
|
['fails on unknown commands instead of treating them as installs', () => {
|
||||||
const result = runCli(['bogus']);
|
const result = runCli(['bogus']);
|
||||||
assert.strictEqual(result.status, 1);
|
assert.strictEqual(result.status, 1);
|
||||||
|
|||||||
@ -48,6 +48,7 @@ function buildExpectedPublishPaths(repoRoot) {
|
|||||||
"scripts/doctor.js",
|
"scripts/doctor.js",
|
||||||
"scripts/status.js",
|
"scripts/status.js",
|
||||||
"scripts/sessions-cli.js",
|
"scripts/sessions-cli.js",
|
||||||
|
"scripts/work-items.js",
|
||||||
"scripts/install-apply.js",
|
"scripts/install-apply.js",
|
||||||
"scripts/install-plan.js",
|
"scripts/install-plan.js",
|
||||||
"scripts/list-installed.js",
|
"scripts/list-installed.js",
|
||||||
@ -110,6 +111,7 @@ function main() {
|
|||||||
for (const requiredPath of [
|
for (const requiredPath of [
|
||||||
"scripts/catalog.js",
|
"scripts/catalog.js",
|
||||||
"scripts/consult.js",
|
"scripts/consult.js",
|
||||||
|
"scripts/work-items.js",
|
||||||
".gemini/GEMINI.md",
|
".gemini/GEMINI.md",
|
||||||
".qwen/QWEN.md",
|
".qwen/QWEN.md",
|
||||||
".claude-plugin/plugin.json",
|
".claude-plugin/plugin.json",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user