@sentropic/track 0.86.0 → 0.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bin.js +14 -1
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +206 -66
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/install-skills.js +10 -10
- package/dist/events/types.d.ts +1 -1
- package/dist/events/types.d.ts.map +1 -1
- package/dist/events/types.js +3 -0
- package/dist/events/types.js.map +1 -1
- package/dist/focus-vendor/cli/index.d.ts +1 -1
- package/dist/ingest/contract.d.ts +2 -2
- package/dist/ingest/contract.d.ts.map +1 -1
- package/dist/ingest/contract.js +13 -2
- package/dist/ingest/contract.js.map +1 -1
- package/dist/ingest/focus-l4.d.ts +1 -1
- package/dist/ingest/focus-l4.d.ts.map +1 -1
- package/dist/ingest/focus-l4.js +10 -2
- package/dist/ingest/focus-l4.js.map +1 -1
- package/dist/ingest/ingest.d.ts.map +1 -1
- package/dist/ingest/ingest.js +8 -0
- package/dist/ingest/ingest.js.map +1 -1
- package/dist/ingest/map.d.ts.map +1 -1
- package/dist/ingest/map.js +3 -0
- package/dist/ingest/map.js.map +1 -1
- package/dist/mcp/server.d.ts +2 -2
- package/dist/mcp/server.js +2 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/decision.d.ts +11 -0
- package/dist/model/decision.d.ts.map +1 -1
- package/dist/model/decision.js +66 -0
- package/dist/model/decision.js.map +1 -1
- package/dist/read/commands.d.ts +8 -14
- package/dist/read/commands.d.ts.map +1 -1
- package/dist/read/commands.js +52 -14
- package/dist/read/commands.js.map +1 -1
- package/dist/read/contract.d.ts +29 -6
- package/dist/read/contract.d.ts.map +1 -1
- package/dist/read/contract.js +46 -8
- package/dist/read/contract.js.map +1 -1
- package/dist/report/build.d.ts +26 -1
- package/dist/report/build.d.ts.map +1 -1
- package/dist/report/build.js +42 -45
- package/dist/report/build.js.map +1 -1
- package/dist/report/directive.d.ts.map +1 -1
- package/dist/report/directive.js +6 -4
- package/dist/report/directive.js.map +1 -1
- package/dist/report/format.d.ts +185 -11
- package/dist/report/format.d.ts.map +1 -1
- package/dist/report/format.js +924 -120
- package/dist/report/format.js.map +1 -1
- package/dist/report/html.d.ts +3 -3
- package/dist/report/html.d.ts.map +1 -1
- package/dist/report/html.js +41 -44
- package/dist/report/html.js.map +1 -1
- package/dist/report/index.d.ts +1 -1
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +1 -1
- package/dist/report/index.js.map +1 -1
- package/dist/report/rollup.d.ts +14 -0
- package/dist/report/rollup.d.ts.map +1 -1
- package/dist/report/rollup.js +32 -0
- package/dist/report/rollup.js.map +1 -1
- package/dist/report/snapshot.d.ts +22 -2
- package/dist/report/snapshot.d.ts.map +1 -1
- package/dist/report/snapshot.js +16 -3
- package/dist/report/snapshot.js.map +1 -1
- package/dist/report/status-by-level.d.ts +1 -1
- package/dist/report/status-by-level.d.ts.map +1 -1
- package/dist/report/status-by-level.js +13 -30
- package/dist/report/status-by-level.js.map +1 -1
- package/dist/state/fold.js +10 -0
- package/dist/state/fold.js.map +1 -1
- package/dist/track.d.ts +6 -4
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +92 -38
- package/dist/track.js.map +1 -1
- package/package.json +2 -2
- package/skills/track-operation/SKILL.md +62 -88
package/dist/cli/bin.js
CHANGED
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
// bin/ — so the installed `track` silently did nothing. A separate entry that just runs is the
|
|
6
6
|
// same posture as track-mcp's cli.ts and cannot regress that way. `index.ts` stays import-only.
|
|
7
7
|
import { runCli } from './index.js';
|
|
8
|
+
// Keep the natural drain that preserves large reports, while treating a downstream closed pipe (for
|
|
9
|
+
// example `track report | head`) as normal CLI termination rather than an uncaught stream error.
|
|
10
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
11
|
+
stream.on('error', (error) => {
|
|
12
|
+
if (error.code === 'EPIPE') {
|
|
13
|
+
process.exitCode ??= 0;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
8
19
|
// `runCli` returns `number | Promise<number>` — the `focus` command is async (it dynamically imports the
|
|
9
20
|
// integrated focus); every other command stays sync and returns a plain number. `Promise.resolve`
|
|
10
21
|
// normalizes both into one exit path, so a sync command still exits with no added microtask churn beyond a
|
|
@@ -13,5 +24,7 @@ Promise.resolve(runCli(process.argv.slice(2), {
|
|
|
13
24
|
cwd: process.cwd(),
|
|
14
25
|
out: (s) => process.stdout.write(s),
|
|
15
26
|
err: (s) => process.stderr.write(s),
|
|
16
|
-
})).then((rc) =>
|
|
27
|
+
})).then((rc) => {
|
|
28
|
+
process.exitCode = rc;
|
|
29
|
+
});
|
|
17
30
|
//# sourceMappingURL=bin.js.map
|
package/dist/cli/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":";AACA,yFAAyF;AACzF,8FAA8F;AAC9F,+FAA+F;AAC/F,+FAA+F;AAC/F,gGAAgG;AAChG,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAEnC,yGAAyG;AACzG,kGAAkG;AAClG,2GAA2G;AAC3G,yBAAyB;AACzB,OAAO,CAAC,OAAO,CACb,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IAC5B,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;IAClB,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACnC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;CACpC,CAAC,
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":";AACA,yFAAyF;AACzF,8FAA8F;AAC9F,+FAA+F;AAC/F,+FAA+F;AAC/F,gGAAgG;AAChG,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAEnC,oGAAoG;AACpG,iGAAiG;AACjG,KAAK,MAAM,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IACtD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE;QAClD,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC3B,OAAO,CAAC,QAAQ,KAAK,CAAC,CAAA;YACtB,OAAM;QACR,CAAC;QACD,MAAM,KAAK,CAAA;IACb,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,yGAAyG;AACzG,kGAAkG;AAClG,2GAA2G;AAC3G,yBAAyB;AACzB,OAAO,CAAC,OAAO,CACb,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IAC5B,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;IAClB,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACnC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;CACpC,CAAC,CAIH,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE;IACZ,OAAO,CAAC,QAAQ,GAAG,EAAE,CAAA;AACvB,CAAC,CAAC,CAAA"}
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AA6CA,MAAM,WAAW,KAAK;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACxB,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACxB,kGAAkG;IAClG,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CACxB;AAqTD,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA8J7E"}
|
package/dist/cli/index.js
CHANGED
|
@@ -9,20 +9,22 @@ import { cmdEventsContains } from './events-contains.js';
|
|
|
9
9
|
import { cmdInstallSkills } from './install-skills.js';
|
|
10
10
|
import { initTrackDir, resolveTrackDir, resolveTrackDirOrNull } from './resolve.js';
|
|
11
11
|
import { DomainError } from '../model/item.js';
|
|
12
|
-
import { formatRows } from '../report/format.js';
|
|
12
|
+
import { displayText, formatRows } from '../report/format.js';
|
|
13
13
|
import { Track } from '../track.js';
|
|
14
|
-
import { BLOCKER_KINDS, BLOCKER_SCOPES, DECISION_KINDS, DISPOSITIONS, EVIDENCE_KINDS, GATES, ITEM_KINDS, ITEM_ROLES,
|
|
14
|
+
import { BLOCKER_KINDS, BLOCKER_SCOPES, DECISION_KINDS, DISPOSITIONS, EVIDENCE_KINDS, GATES, ITEM_KINDS, ITEM_ROLES, REALIZE_TARGETS, RESOLUTION_RULES, RESULTS, ROLE_CHANGE_TARGETS, SPEC_TARGETS, } from '../ingest/contract.js';
|
|
15
15
|
import { ingest } from '../ingest/ingest.js';
|
|
16
16
|
import { applyRestructurePlan } from './restructure-apply.js';
|
|
17
17
|
import { TrackReader } from '../read/contract.js';
|
|
18
|
-
import { queryText, reportText, statusText } from '../read/commands.js';
|
|
18
|
+
import { queryText, reportHtml, reportInline, reportText, resolveHandle, statusText } from '../read/commands.js';
|
|
19
19
|
import { STATUS_LEVELS } from '../report/status-by-level.js';
|
|
20
20
|
import { renderSnapshot } from '../report/snapshot.js';
|
|
21
|
-
import { generateAiReport } from '../report/ai-report.js';
|
|
22
21
|
import { VERSION } from '../version.js';
|
|
23
22
|
import { durableWorkspaceId } from '../workspace-id.js';
|
|
24
23
|
import { desyncFindings } from './desync.js';
|
|
25
24
|
const USAGE = `usage: track <command>
|
|
25
|
+
--help | help
|
|
26
|
+
global read/store override: --track-dir <directory-containing-events.jsonl> (or TRACK_DIR)
|
|
27
|
+
may appear before or after the command; it redirects reads AND writes to that directory
|
|
26
28
|
--version | -v
|
|
27
29
|
init
|
|
28
30
|
item new --kind <feature|bug|chore> --title <t> --workspace <w> [--body <b>] [--parent <id>] [--role <workpackage|spec-phase|stream>] [--accountable <a>] [--responsible <a,a>] [--engagement-ref <e>]
|
|
@@ -35,9 +37,11 @@ const USAGE = `usage: track <command>
|
|
|
35
37
|
item assign-code <itemId> --code <c> [--client-token <t>]
|
|
36
38
|
item show <itemId>
|
|
37
39
|
item ls [--workspace <w>] [--kind <feature|bug|chore>] [--format json|text|md]
|
|
38
|
-
decision new --kind <orientation|commitment> --title <t> --workspace <w> --targets <id,id>
|
|
39
|
-
decision
|
|
40
|
-
decision
|
|
40
|
+
decision new --kind <orientation|commitment> --title <t> --workspace <w> --targets <id,id> --context <c> --options-json <json> --recommendation <optionId> --rationale <r> [--accountable <a>] [--engagement-ref <e>]
|
|
41
|
+
decision ls [--workspace <w>] [--outcome <pending|go|no-go|deferred>] [--format json|text|md] [--commit <sha>]
|
|
42
|
+
decision outcome <decisionId> deferred
|
|
43
|
+
decision select <decisionId> <optionId> [--outcome <go|no-go>]
|
|
44
|
+
decision dossier <decisionId> [--context <c>] [--options-json <json> --recommendation <optionId> --rationale <r>]
|
|
41
45
|
decision disposition <itemId> <orientation|commitment> <required|skipped|not-applicable>
|
|
42
46
|
decision add-artifact <decisionId> --kind <h2a-decision-dossier|rendered-view|mockup> [--negotiation-ref <n>] [--dossier-hash <h>] [--view-ref <v>] [--source-dossier-hash <h>] [--label <l>] [--client-token <t>]
|
|
43
47
|
blocker raise --target <id> --kind <decision|dependency> [--ref <id>] [--reason <r>] [--rule <linked-done|linked-accepted|manual>] [--scope <intra|extra>] [--engagement-ref <e>]
|
|
@@ -50,7 +54,7 @@ const USAGE = `usage: track <command>
|
|
|
50
54
|
accept waive <criterionId> --reason <r>
|
|
51
55
|
consolidate --items <id,id> --commit <mergeCommit> [--client-token <t>]
|
|
52
56
|
priority assess <itemId> --ubv <n> --tc <n> --rr <n> --js <n>
|
|
53
|
-
report [--decisions] [--require-accepted] [--active-roster] [--wp|--flat] [--inline] [--width <n>] [--level <spec|plan|wp|lot|task>] [--raw] [--format json|text|md|html] [--commit <sha>]
|
|
57
|
+
report [--decisions] [--require-accepted] [--active-roster] [--wp|--flat] [--inline] [--width <n>] [--level <spec|plan|wp|lot|task>] [--raw] [--resolve <handle>] [--sub-wp] [--format json|text|md|html] [--commit <sha>] [--now <iso>]
|
|
54
58
|
snapshot [--require-accepted] [--format json|text|md] [--commit <sha>]
|
|
55
59
|
export-graph [--repo-key <repo:key>] [--source-id <id>] [--observed-at <iso>]
|
|
56
60
|
query [--kind <k>] [--role <workpackage|spec-phase|stream>] [--workspace <w>] [--bucket <AWAITED|DROPPED|DONE|TO-DO>] [--realization <r>] [--acceptance <a>] [--format json|text|md] [--commit <sha>]
|
|
@@ -66,7 +70,7 @@ const USAGE = `usage: track <command>
|
|
|
66
70
|
install-skills --host <claude|codex|gemini|agy|all> [--scope user|project] [--force]
|
|
67
71
|
workspace-id [--cwd <path>]
|
|
68
72
|
`;
|
|
69
|
-
// Write enums (ITEM_KINDS, SPEC_TARGETS, REALIZE_TARGETS, DECISION_KINDS,
|
|
73
|
+
// Write enums (ITEM_KINDS, SPEC_TARGETS, REALIZE_TARGETS, DECISION_KINDS, GATES, DISPOSITIONS,
|
|
70
74
|
// BLOCKER_KINDS, RESOLUTION_RULES, EVIDENCE_KINDS, RESULTS) are sourced from the ingest contract — the
|
|
71
75
|
// SINGLE source, so the CLI's `oneOf` checks and the WorkEvent mapper cannot diverge on accepted values.
|
|
72
76
|
// (`linked-accepted` openness is DERIVED at report/query time vs `--commit`, v2.2a hybrid-A; see
|
|
@@ -74,6 +78,7 @@ const USAGE = `usage: track <command>
|
|
|
74
78
|
const REALIZATIONS = ['to-do', 'in-progress', 'done', 'cancelled', 'rejected'];
|
|
75
79
|
const FROM_FORMATS = ['junit', 'json'];
|
|
76
80
|
const BUCKETS_ARG = ['AWAITED', 'DROPPED', 'DONE', 'TO-DO'];
|
|
81
|
+
const DECISION_OUTCOMES = ['pending', 'go', 'no-go', 'deferred'];
|
|
77
82
|
// `n/a` is decision-only; `query` projects non-decision rows, so it would never match.
|
|
78
83
|
const ACCEPTANCES = ['fail', 'waived', 'unknown', 'stale', 'pass'];
|
|
79
84
|
// The DossierArtifact discriminator (M5 §3.1). CLI-local: the union SHAPE is validated fail-closed in the
|
|
@@ -241,6 +246,19 @@ function num(flags, key) {
|
|
|
241
246
|
throw new DomainError(`--${key} must be a number`);
|
|
242
247
|
return n;
|
|
243
248
|
}
|
|
249
|
+
function dossierOptions(raw) {
|
|
250
|
+
let parsed;
|
|
251
|
+
try {
|
|
252
|
+
parsed = JSON.parse(raw);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
throw new DomainError('--options-json must be a JSON array of existing Option objects');
|
|
256
|
+
}
|
|
257
|
+
if (!Array.isArray(parsed)) {
|
|
258
|
+
throw new DomainError('--options-json must be a JSON array of existing Option objects');
|
|
259
|
+
}
|
|
260
|
+
return parsed;
|
|
261
|
+
}
|
|
244
262
|
/** Validate a positional/flag against an allowed enum (CLI-boundary input validation). */
|
|
245
263
|
function oneOf(value, allowed, name) {
|
|
246
264
|
if (value === undefined || !allowed.includes(value)) {
|
|
@@ -290,10 +308,28 @@ function extractTrackDirFlag(argv) {
|
|
|
290
308
|
}
|
|
291
309
|
return trackDirFlag !== undefined ? { trackDirFlag, rest } : { rest };
|
|
292
310
|
}
|
|
311
|
+
const REPORT_USAGE = `usage: track report [--raw] [--wp] [--flat] [--inline|--width <40..240>] [--decisions] [--active-roster] [--require-accepted] [--resolve <handle>] [--commit <sha>] [--now <iso>] [--sub-wp] [--format json|text|md|html] [--track-dir <directory-containing-events.jsonl>]
|
|
312
|
+
|
|
313
|
+
--resolve <handle> resolves a report handle (a positional [n.m] row handle, or a D#/Q# dossier number) back to its item id. It is the one command the report documents for acting on a row without printing a ULID in a column. Handles are positional and per-report: resolve them against the same log and baseline the report was rendered from.
|
|
314
|
+
|
|
315
|
+
--sub-wp lists sub-WP rows beside their parent. Without it, sub-levels are aggregated into their parent on a long window (>= 14 days) and listed on a short one — the WP is the reading unit of a long report.
|
|
316
|
+
|
|
317
|
+
--now <iso> pins the window's upper bound (default: the wall clock). The report's period always runs from the first recorded event to that bound; pin it to reproduce a committed fixture byte for byte.
|
|
318
|
+
|
|
319
|
+
--track-dir is a global override and may appear before or after the command. It selects the directory that contains events.jsonl; it is especially useful for a read-only fixture. TRACK_DIR is the environment equivalent.
|
|
320
|
+
`;
|
|
293
321
|
export function runCli(rawArgv, io) {
|
|
294
322
|
const { trackDirFlag, rest: argv } = extractTrackDirFlag(rawArgv);
|
|
295
323
|
const cmd = argv[0];
|
|
296
324
|
const rest = argv.slice(1);
|
|
325
|
+
if (cmd === '--help' || cmd === 'help' || cmd === undefined) {
|
|
326
|
+
io.out(USAGE);
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
if (cmd === 'report' && rest.length === 1 && rest[0] === '--help') {
|
|
330
|
+
io.out(REPORT_USAGE);
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
297
333
|
const trackDirEnv = process.env['TRACK_DIR'];
|
|
298
334
|
const resolveOpts = {
|
|
299
335
|
cwd: io.cwd,
|
|
@@ -301,6 +337,19 @@ export function runCli(rawArgv, io) {
|
|
|
301
337
|
...(trackDirEnv !== undefined ? { env: trackDirEnv } : {}),
|
|
302
338
|
};
|
|
303
339
|
try {
|
|
340
|
+
// `decision ls` is an inspection surface, not a write verb. Route it through the same serve-empty
|
|
341
|
+
// read path as report/query so it never creates a sidecar and can be used by a fresh reporting agent.
|
|
342
|
+
if (cmd === 'decision' && rest[0] === 'ls') {
|
|
343
|
+
const trackDir = resolveTrackDirOrNull(resolveOpts);
|
|
344
|
+
if (trackDir === null) {
|
|
345
|
+
io.err(`track: no .track resolved from ${io.cwd}. Run \`track init\` to create one ` +
|
|
346
|
+
`(the ONLY command that does), or pass --track-dir / TRACK_DIR. Serving an empty view.\n`);
|
|
347
|
+
}
|
|
348
|
+
return cmdDecisionLs(rest, {
|
|
349
|
+
io,
|
|
350
|
+
eventsPath: trackDir !== null ? eventsPathOf(trackDir) : eventsPathOf(join(resolve(io.cwd), '.track')),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
304
353
|
switch (cmd) {
|
|
305
354
|
case '--version':
|
|
306
355
|
case '-v':
|
|
@@ -659,7 +708,12 @@ function cmdDecision(args, ctx) {
|
|
|
659
708
|
title: req(flags, 'title'),
|
|
660
709
|
workspace: req(flags, 'workspace'),
|
|
661
710
|
targets: req(flags, 'targets').split(',').map((s) => s.trim()).filter(Boolean),
|
|
662
|
-
dossier: {
|
|
711
|
+
dossier: {
|
|
712
|
+
context: req(flags, 'context'),
|
|
713
|
+
options: dossierOptions(req(flags, 'options-json')),
|
|
714
|
+
qa: [],
|
|
715
|
+
recommendation: { optionId: req(flags, 'recommendation'), rationale: req(flags, 'rationale') },
|
|
716
|
+
},
|
|
663
717
|
...(opt(flags, 'accountable') !== undefined ? { accountable: req(flags, 'accountable') } : {}),
|
|
664
718
|
...(opt(flags, 'engagement-ref') !== undefined ? { engagementRef: req(flags, 'engagement-ref') } : {}),
|
|
665
719
|
});
|
|
@@ -667,16 +721,37 @@ function cmdDecision(args, ctx) {
|
|
|
667
721
|
return 0;
|
|
668
722
|
}
|
|
669
723
|
if (sub === 'outcome') {
|
|
670
|
-
track.setOutcome(positional[0], oneOf(positional[1],
|
|
724
|
+
track.setOutcome(positional[0], oneOf(positional[1], ['deferred'], 'outcome'));
|
|
671
725
|
io.out('ok\n');
|
|
672
726
|
return 0;
|
|
673
727
|
}
|
|
674
728
|
if (sub === 'dossier') {
|
|
675
|
-
//
|
|
729
|
+
// Merge: a context-only edit preserves a structured dossier. A legacy dossier must provide the
|
|
730
|
+
// full options/recommendation triplet here; the facade validates it fail-closed.
|
|
676
731
|
const current = track.state().decisions.get(positional[0])?.dossier;
|
|
677
732
|
if (current === undefined)
|
|
678
733
|
throw new DomainError(`unknown decision ${positional[0]}`);
|
|
679
|
-
|
|
734
|
+
const optionJson = opt(flags, 'options-json');
|
|
735
|
+
const recommendation = opt(flags, 'recommendation');
|
|
736
|
+
const rationale = opt(flags, 'rationale');
|
|
737
|
+
if ((recommendation === undefined) !== (rationale === undefined)) {
|
|
738
|
+
throw new DomainError('--recommendation and --rationale must be supplied together');
|
|
739
|
+
}
|
|
740
|
+
if (optionJson !== undefined && recommendation === undefined && current.recommendation === undefined) {
|
|
741
|
+
throw new DomainError('migrating a legacy dossier requires --recommendation and --rationale');
|
|
742
|
+
}
|
|
743
|
+
track.reviseDossier(positional[0], {
|
|
744
|
+
...current,
|
|
745
|
+
context: opt(flags, 'context') ?? current.context,
|
|
746
|
+
...(optionJson !== undefined ? { options: dossierOptions(optionJson) } : {}),
|
|
747
|
+
...(recommendation !== undefined ? { recommendation: { optionId: recommendation, rationale: rationale } } : {}),
|
|
748
|
+
});
|
|
749
|
+
io.out('ok\n');
|
|
750
|
+
return 0;
|
|
751
|
+
}
|
|
752
|
+
if (sub === 'select') {
|
|
753
|
+
const outcome = opt(flags, 'outcome');
|
|
754
|
+
track.selectDecisionOption(positional[0], positional[1], outcome === undefined ? 'go' : oneOf(outcome, ['go', 'no-go'], '--outcome'));
|
|
680
755
|
io.out('ok\n');
|
|
681
756
|
return 0;
|
|
682
757
|
}
|
|
@@ -719,7 +794,7 @@ function cmdDecision(args, ctx) {
|
|
|
719
794
|
io.out('ok\n');
|
|
720
795
|
return 0;
|
|
721
796
|
}
|
|
722
|
-
io.err('usage: track decision <new|outcome|dossier|disposition|add-artifact>\n');
|
|
797
|
+
io.err('usage: track decision <new|outcome|select|dossier|disposition|add-artifact>\n');
|
|
723
798
|
return 2;
|
|
724
799
|
}
|
|
725
800
|
function cmdBlocker(args, ctx) {
|
|
@@ -857,61 +932,58 @@ function cmdReport(args, ctx) {
|
|
|
857
932
|
const { positional, flags } = parseFlags(args);
|
|
858
933
|
if (positional.length > 0)
|
|
859
934
|
throw new DomainError(`unexpected report argument(s): ${positional.join(' ')}`);
|
|
860
|
-
for (const name of ['commit', 'format', 'level', 'width'])
|
|
935
|
+
for (const name of ['commit', 'format', 'level', 'width', 'resolve', 'now'])
|
|
861
936
|
assertValueFlag(flags, name);
|
|
937
|
+
// Criterion 10b — the one documented command that turns a short report handle back into an item.
|
|
938
|
+
if (opt(flags, 'resolve') !== undefined) {
|
|
939
|
+
assertOnlyFlags(flags, ['resolve', 'commit', 'require-accepted']);
|
|
940
|
+
io.out(resolveHandle(new TrackReader(ctx.eventsPath), {
|
|
941
|
+
baselineCommit: resolveCommit(io.cwd, opt(flags, 'commit')),
|
|
942
|
+
requireAccepted: assertBooleanFlag(flags, 'require-accepted'),
|
|
943
|
+
}, req(flags, 'resolve')));
|
|
944
|
+
return 0;
|
|
945
|
+
}
|
|
862
946
|
const raw = assertBooleanFlag(flags, 'raw');
|
|
863
947
|
if (raw) {
|
|
864
948
|
assertOnlyFlags(flags, ['raw', 'commit', 'require-accepted', 'format']);
|
|
865
949
|
return emitSnapshot(flags, ctx, true);
|
|
866
950
|
}
|
|
867
|
-
// Reads go through the shared TrackReader command layer (same path the MCP server uses).
|
|
868
|
-
const reader = new TrackReader(ctx.eventsPath);
|
|
869
951
|
// Scope §A/§B — `--level <spec|plan|wp|lot|task>` switches to the status(level) projection.
|
|
870
|
-
// Otherwise
|
|
952
|
+
// Otherwise the WP/table conductor is the default; `--flat` is the legacy opt-out.
|
|
871
953
|
if (opt(flags, 'level') !== undefined) {
|
|
872
954
|
assertOnlyFlags(flags, ['level', 'commit', 'require-accepted', 'format']);
|
|
873
955
|
assertBooleanFlag(flags, 'require-accepted');
|
|
874
|
-
io.out(statusText(
|
|
956
|
+
io.out(statusText(new TrackReader(ctx.eventsPath), oneOf(req(flags, 'level'), STATUS_LEVELS, '--level'), {
|
|
875
957
|
baselineCommit: resolveCommit(io.cwd, opt(flags, 'commit')),
|
|
876
958
|
requireAccepted: assertBooleanFlag(flags, 'require-accepted'),
|
|
877
959
|
}, fmt(flags)));
|
|
878
960
|
return 0;
|
|
879
961
|
}
|
|
962
|
+
assertOnlyFlags(flags, [
|
|
963
|
+
'commit', 'require-accepted', 'decisions', 'active-roster', 'wp', 'flat', 'inline', 'width', 'format', 'now',
|
|
964
|
+
'sub-wp',
|
|
965
|
+
]);
|
|
880
966
|
const rawFormat = opt(flags, 'format');
|
|
967
|
+
if (rawFormat !== undefined && !['json', 'text', 'md', 'html'].includes(rawFormat)) {
|
|
968
|
+
throw new DomainError('--format must be one of: json|text|md|html');
|
|
969
|
+
}
|
|
881
970
|
const widthArg = opt(flags, 'width');
|
|
882
971
|
const inlineFlag = assertBooleanFlag(flags, 'inline');
|
|
883
972
|
const inline = inlineFlag || widthArg !== undefined;
|
|
884
|
-
|
|
973
|
+
const format = oneOf(rawFormat ?? 'text', ['json', 'text', 'md', 'html'], '--format');
|
|
974
|
+
if (inline && format !== 'text')
|
|
885
975
|
throw new DomainError('--inline/--width accepts no --format, or --format text');
|
|
886
|
-
}
|
|
887
|
-
if (rawFormat === 'html' && inline)
|
|
888
|
-
throw new DomainError('--format html rejects --inline/--width');
|
|
889
|
-
// Frozen legacy path. Keep this call and option derivation byte-for-byte equivalent to the former JSON
|
|
890
|
-
// branch: it never enters context collection or the adapter.
|
|
891
|
-
if (rawFormat === 'json') {
|
|
892
|
-
assertOnlyFlags(flags, ['commit', 'require-accepted', 'decisions', 'active-roster', 'wp', 'flat', 'format']);
|
|
893
|
-
io.out(reportText(reader, {
|
|
894
|
-
baselineCommit: resolveCommit(io.cwd, opt(flags, 'commit')),
|
|
895
|
-
requireAccepted: flags['require-accepted'] === true,
|
|
896
|
-
decisions: flags['decisions'] === true,
|
|
897
|
-
wpTree: flags['wp'] === true,
|
|
898
|
-
activeRoster: flags['active-roster'] === true,
|
|
899
|
-
}, 'json'));
|
|
900
|
-
return 0;
|
|
901
|
-
}
|
|
902
|
-
assertOnlyFlags(flags, [
|
|
903
|
-
'commit', 'require-accepted', 'decisions', 'active-roster', 'wp', 'flat', 'inline', 'width', 'format',
|
|
904
|
-
]);
|
|
905
|
-
if (rawFormat !== undefined && !['text', 'md', 'html'].includes(rawFormat)) {
|
|
906
|
-
throw new DomainError('--format must be one of: json|text|md|html');
|
|
907
|
-
}
|
|
908
976
|
const requireAccepted = assertBooleanFlag(flags, 'require-accepted');
|
|
909
|
-
const
|
|
977
|
+
const requestedDecisions = assertBooleanFlag(flags, 'decisions');
|
|
910
978
|
const activeRoster = assertBooleanFlag(flags, 'active-roster');
|
|
911
979
|
const wp = assertBooleanFlag(flags, 'wp');
|
|
912
980
|
const flat = assertBooleanFlag(flags, 'flat');
|
|
913
981
|
if (wp && flat)
|
|
914
982
|
throw new DomainError('--wp and --flat are mutually exclusive');
|
|
983
|
+
if (format === 'json' && flat)
|
|
984
|
+
throw new DomainError('--flat is only meaningful for text or md reports');
|
|
985
|
+
if (format === 'html' && flat)
|
|
986
|
+
throw new DomainError('--format html is always the deterministic conductor and rejects --flat');
|
|
915
987
|
let width;
|
|
916
988
|
if (widthArg !== undefined) {
|
|
917
989
|
if (!/^\d+$/u.test(widthArg))
|
|
@@ -920,28 +992,41 @@ function cmdReport(args, ctx) {
|
|
|
920
992
|
if (width < 40 || width > 240)
|
|
921
993
|
throw new DomainError('--width must be an integer in [40,240]');
|
|
922
994
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
995
|
+
// Every format is a deterministic read over the folded log. The default human view is
|
|
996
|
+
// the conductor; --flat explicitly requests the legacy bucket projection. JSON preserves
|
|
997
|
+
// its established flat machine contract unless --wp is explicit, so --flat is rejected there
|
|
998
|
+
// instead of being silently accepted as a no-op. Contextual prose remains advisory agent work;
|
|
999
|
+
// no report format invokes an adapter, gateway, subprocess, or model.
|
|
1000
|
+
const options = {
|
|
1001
|
+
baselineCommit: resolveCommit(io.cwd, opt(flags, 'commit')),
|
|
1002
|
+
requireAccepted,
|
|
1003
|
+
// The owner-facing conductor always classifies decision dossiers. JSON keeps its
|
|
1004
|
+
// established opt-in decision payload, while --flat retains the legacy opt-in.
|
|
1005
|
+
decisions: requestedDecisions || (!flat && format !== 'json'),
|
|
1006
|
+
wpTree: wp || (!flat && format !== 'json'),
|
|
1007
|
+
activeRoster,
|
|
1008
|
+
};
|
|
1009
|
+
// Criterion 21 — the report's window runs from the first recorded event to NOW. The clock is injected
|
|
1010
|
+
// HERE (the same boundary pattern as `workspace-activity --now`) so the library stays clockless and a
|
|
1011
|
+
// committed fixture stays byte-reproducible by pinning `--now`.
|
|
1012
|
+
const nowArg = opt(flags, 'now');
|
|
1013
|
+
if (nowArg !== undefined && Number.isNaN(new Date(nowArg).getTime())) {
|
|
1014
|
+
throw new DomainError('--now must be an ISO timestamp');
|
|
1015
|
+
}
|
|
1016
|
+
const now = nowArg ?? new Date().toISOString();
|
|
1017
|
+
// Criterion 25 — the EXPLICIT owner request for sub-WP rows. Without it the reading unit follows the
|
|
1018
|
+
// window: the WP on a long one, the sub-level on a short one.
|
|
1019
|
+
const subWp = assertBooleanFlag(flags, 'sub-wp');
|
|
1020
|
+
const reader = new TrackReader(ctx.eventsPath);
|
|
1021
|
+
if (inline) {
|
|
1022
|
+
io.out(reportInline(reader, options, width === undefined ? {} : { width }));
|
|
1023
|
+
}
|
|
1024
|
+
else if (format === 'html') {
|
|
1025
|
+
io.out(reportHtml(reader, options, now, subWp));
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
io.out(reportText(reader, options, format, now, subWp));
|
|
1029
|
+
}
|
|
945
1030
|
return 0;
|
|
946
1031
|
}
|
|
947
1032
|
function emitSnapshot(flags, ctx, allowRaw) {
|
|
@@ -996,6 +1081,43 @@ function cmdQuery(args, ctx) {
|
|
|
996
1081
|
}, { baselineCommit: resolveCommit(io.cwd, opt(flags, 'commit')) }, fmt(flags)));
|
|
997
1082
|
return 0;
|
|
998
1083
|
}
|
|
1084
|
+
/**
|
|
1085
|
+
* List decision dossiers without a renderer cap. `structure` makes the prose-only migration state explicit;
|
|
1086
|
+
* only a structured dossier has durable alternatives + a durable recommendation to render as a choice.
|
|
1087
|
+
*/
|
|
1088
|
+
function cmdDecisionLs(args, ctx) {
|
|
1089
|
+
const { io } = ctx;
|
|
1090
|
+
const { positional, flags } = parseFlags(args.slice(1));
|
|
1091
|
+
if (positional.length > 0)
|
|
1092
|
+
throw new DomainError(`unexpected decision ls argument(s): ${positional.join(' ')}`);
|
|
1093
|
+
assertOnlyFlags(flags, ['workspace', 'outcome', 'format', 'commit']);
|
|
1094
|
+
for (const name of ['workspace', 'outcome', 'format', 'commit'])
|
|
1095
|
+
assertValueFlag(flags, name);
|
|
1096
|
+
const format = fmt(flags);
|
|
1097
|
+
const baselineCommit = resolveCommit(io.cwd, opt(flags, 'commit'));
|
|
1098
|
+
const reader = new TrackReader(ctx.eventsPath);
|
|
1099
|
+
const workspace = opt(flags, 'workspace');
|
|
1100
|
+
const outcome = opt(flags, 'outcome') !== undefined ? oneOf(req(flags, 'outcome'), DECISION_OUTCOMES, '--outcome') : undefined;
|
|
1101
|
+
const rows = reader.decisionDossiers({ baselineCommit })
|
|
1102
|
+
.filter((decision) => (workspace === undefined || decision.workspace === workspace) && (outcome === undefined || decision.outcome === outcome))
|
|
1103
|
+
.map(({ dossier, ...decision }) => ({
|
|
1104
|
+
...decision,
|
|
1105
|
+
options: dossier.options,
|
|
1106
|
+
...(dossier.recommendation !== undefined ? { recommendation: dossier.recommendation } : {}),
|
|
1107
|
+
}));
|
|
1108
|
+
if (format === 'json') {
|
|
1109
|
+
io.out(`${JSON.stringify(rows, null, 2)}\n`);
|
|
1110
|
+
return 0;
|
|
1111
|
+
}
|
|
1112
|
+
const lines = rows.map((decision) => {
|
|
1113
|
+
const safe = (value) => displayText(value, format);
|
|
1114
|
+
const recommendation = safe(decision.recommendation?.optionId ?? '-');
|
|
1115
|
+
const line = `${safe(decision.id)} · ${safe(decision.workspace)} · ${safe(decision.outcome)} · ${safe(decision.structure ?? 'unstructured')} · options:${decision.options.length} · recommendation:${recommendation} — ${safe(decision.title)}`;
|
|
1116
|
+
return format === 'md' ? `- ${line}` : line;
|
|
1117
|
+
});
|
|
1118
|
+
io.out(lines.length > 0 ? `${lines.join('\n')}\n` : '');
|
|
1119
|
+
return 0;
|
|
1120
|
+
}
|
|
999
1121
|
/**
|
|
1000
1122
|
* `track workspace-activity --workspace <id> [--baseline-commit <sha>] [--now <iso>] [--idle-ms <ms>]
|
|
1001
1123
|
* [--format json|text]` — a poll surface over the shipped, CLOCKLESS `TrackReader.workspaceActivity`
|
|
@@ -1079,6 +1201,15 @@ async function cmdFocus(args, ctx, _noStore) {
|
|
|
1079
1201
|
const { io } = ctx;
|
|
1080
1202
|
const FOCUS_USAGE = 'usage: track focus <decision-id> --workspace <w> [--format terminal|md|html] [--baseline-commit <sha>]\n';
|
|
1081
1203
|
const { positional, flags } = parseFlags(args);
|
|
1204
|
+
try {
|
|
1205
|
+
assertOnlyFlags(flags, ['workspace', 'format', 'baseline-commit']);
|
|
1206
|
+
for (const name of ['workspace', 'format', 'baseline-commit'])
|
|
1207
|
+
assertValueFlag(flags, name);
|
|
1208
|
+
}
|
|
1209
|
+
catch (error) {
|
|
1210
|
+
io.err(`error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1211
|
+
return 2;
|
|
1212
|
+
}
|
|
1082
1213
|
// decision-id is positional + REQUIRED; --workspace is a REQUIRED flag (both gate at the CLI boundary
|
|
1083
1214
|
// with rc=2 + usage, never reaching focus). Validate BEFORE the dynamic import so a usage error is
|
|
1084
1215
|
// independent of whether focus is installed.
|
|
@@ -1098,6 +1229,15 @@ async function cmdFocus(args, ctx, _noStore) {
|
|
|
1098
1229
|
io.err(`error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1099
1230
|
return 2;
|
|
1100
1231
|
}
|
|
1232
|
+
const baselineCommit = resolveCommit(io.cwd, opt(flags, 'baseline-commit'));
|
|
1233
|
+
const decision = new TrackReader(ctx.eventsPath)
|
|
1234
|
+
.report({ baselineCommit, decisions: true })
|
|
1235
|
+
.decisions
|
|
1236
|
+
?.find((candidate) => candidate.id === decisionId);
|
|
1237
|
+
if (decision !== undefined && decision.workspace !== workspace) {
|
|
1238
|
+
io.err(`error: decision ${decisionId} belongs to workspace ${decision.workspace}, not ${workspace}\n`);
|
|
1239
|
+
return 3;
|
|
1240
|
+
}
|
|
1101
1241
|
// Load the focus render binding + core. focus is an integrated dependency → a MODULE_NOT_FOUND means the
|
|
1102
1242
|
// integrated focus renderer is unavailable: map it to rc=1 + a helpful hint (NOT a stack trace).
|
|
1103
1243
|
let focusTrack;
|
|
@@ -1118,7 +1258,7 @@ async function cmdFocus(args, ctx, _noStore) {
|
|
|
1118
1258
|
try {
|
|
1119
1259
|
// PURE / read-only / clockless: `readAt` is supplied at the CLI boundary (track holds no clock), the
|
|
1120
1260
|
// baseline commit resolves HEAD/refs/short-SHA → 40-char, and `ctx.eventsPath` is the single store.
|
|
1121
|
-
const doc = focusTrack.readDecisionDossier(ctx.eventsPath, { workspace, baselineCommit
|
|
1261
|
+
const doc = focusTrack.readDecisionDossier(ctx.eventsPath, { workspace, baselineCommit, decisionId }, new Date().toISOString());
|
|
1122
1262
|
const rendered = format === 'md' ? core.renderMd(doc) : format === 'html' ? core.renderHtml(doc, HTML_HOOKS) : core.renderTerminal(doc);
|
|
1123
1263
|
io.out(rendered.endsWith('\n') ? rendered : `${rendered}\n`);
|
|
1124
1264
|
return 0;
|