@sentropic/track 0.86.0 → 0.87.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.
Files changed (71) hide show
  1. package/dist/cli/bin.js +14 -1
  2. package/dist/cli/bin.js.map +1 -1
  3. package/dist/cli/index.d.ts.map +1 -1
  4. package/dist/cli/index.js +162 -64
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/cli/install-skills.js +10 -10
  7. package/dist/events/types.d.ts +1 -1
  8. package/dist/events/types.d.ts.map +1 -1
  9. package/dist/events/types.js +3 -0
  10. package/dist/events/types.js.map +1 -1
  11. package/dist/focus-vendor/cli/index.d.ts +1 -1
  12. package/dist/ingest/contract.d.ts +2 -2
  13. package/dist/ingest/contract.d.ts.map +1 -1
  14. package/dist/ingest/contract.js +13 -2
  15. package/dist/ingest/contract.js.map +1 -1
  16. package/dist/ingest/focus-l4.d.ts +1 -1
  17. package/dist/ingest/focus-l4.d.ts.map +1 -1
  18. package/dist/ingest/focus-l4.js +10 -2
  19. package/dist/ingest/focus-l4.js.map +1 -1
  20. package/dist/ingest/ingest.d.ts.map +1 -1
  21. package/dist/ingest/ingest.js +8 -0
  22. package/dist/ingest/ingest.js.map +1 -1
  23. package/dist/ingest/map.d.ts.map +1 -1
  24. package/dist/ingest/map.js +3 -0
  25. package/dist/ingest/map.js.map +1 -1
  26. package/dist/mcp/server.d.ts +2 -2
  27. package/dist/mcp/server.js +2 -2
  28. package/dist/mcp/server.js.map +1 -1
  29. package/dist/model/decision.d.ts +11 -0
  30. package/dist/model/decision.d.ts.map +1 -1
  31. package/dist/model/decision.js +66 -0
  32. package/dist/model/decision.js.map +1 -1
  33. package/dist/read/commands.d.ts +2 -2
  34. package/dist/read/commands.d.ts.map +1 -1
  35. package/dist/read/commands.js +14 -11
  36. package/dist/read/commands.js.map +1 -1
  37. package/dist/read/contract.d.ts +18 -6
  38. package/dist/read/contract.d.ts.map +1 -1
  39. package/dist/read/contract.js +30 -8
  40. package/dist/read/contract.js.map +1 -1
  41. package/dist/report/build.d.ts +20 -1
  42. package/dist/report/build.d.ts.map +1 -1
  43. package/dist/report/build.js +40 -14
  44. package/dist/report/build.js.map +1 -1
  45. package/dist/report/directive.d.ts.map +1 -1
  46. package/dist/report/directive.js +6 -4
  47. package/dist/report/directive.js.map +1 -1
  48. package/dist/report/format.d.ts +15 -6
  49. package/dist/report/format.d.ts.map +1 -1
  50. package/dist/report/format.js +160 -65
  51. package/dist/report/format.js.map +1 -1
  52. package/dist/report/html.d.ts +2 -2
  53. package/dist/report/html.d.ts.map +1 -1
  54. package/dist/report/html.js +2 -2
  55. package/dist/report/html.js.map +1 -1
  56. package/dist/report/snapshot.d.ts +2 -1
  57. package/dist/report/snapshot.d.ts.map +1 -1
  58. package/dist/report/snapshot.js +7 -1
  59. package/dist/report/snapshot.js.map +1 -1
  60. package/dist/report/status-by-level.d.ts +1 -1
  61. package/dist/report/status-by-level.d.ts.map +1 -1
  62. package/dist/report/status-by-level.js +13 -30
  63. package/dist/report/status-by-level.js.map +1 -1
  64. package/dist/state/fold.js +10 -0
  65. package/dist/state/fold.js.map +1 -1
  66. package/dist/track.d.ts +6 -4
  67. package/dist/track.d.ts.map +1 -1
  68. package/dist/track.js +92 -38
  69. package/dist/track.js.map +1 -1
  70. package/package.json +2 -2
  71. package/skills/track-operation/SKILL.md +60 -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) => process.exit(rc));
27
+ })).then((rc) => {
28
+ process.exitCode = rc;
29
+ });
17
30
  //# sourceMappingURL=bin.js.map
@@ -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,CACH,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA"}
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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AA+CA,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;AAuRD,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuI7E"}
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;AAuSD,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsJ7E"}
package/dist/cli/index.js CHANGED
@@ -9,16 +9,15 @@ 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, OUTCOMES, REALIZE_TARGETS, RESOLUTION_RULES, RESULTS, ROLE_CHANGE_TARGETS, SPEC_TARGETS, } from '../ingest/contract.js';
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, 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';
@@ -35,9 +34,11 @@ const USAGE = `usage: track <command>
35
34
  item assign-code <itemId> --code <c> [--client-token <t>]
36
35
  item show <itemId>
37
36
  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> [--context <c>] [--accountable <a>] [--engagement-ref <e>]
39
- decision outcome <decisionId> <go|no-go|deferred>
40
- decision dossier <decisionId> --context <c>
37
+ 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>]
38
+ decision ls [--workspace <w>] [--outcome <pending|go|no-go|deferred>] [--format json|text|md] [--commit <sha>]
39
+ decision outcome <decisionId> deferred
40
+ decision select <decisionId> <optionId> [--outcome <go|no-go>]
41
+ decision dossier <decisionId> [--context <c>] [--options-json <json> --recommendation <optionId> --rationale <r>]
41
42
  decision disposition <itemId> <orientation|commitment> <required|skipped|not-applicable>
42
43
  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
44
  blocker raise --target <id> --kind <decision|dependency> [--ref <id>] [--reason <r>] [--rule <linked-done|linked-accepted|manual>] [--scope <intra|extra>] [--engagement-ref <e>]
@@ -66,7 +67,7 @@ const USAGE = `usage: track <command>
66
67
  install-skills --host <claude|codex|gemini|agy|all> [--scope user|project] [--force]
67
68
  workspace-id [--cwd <path>]
68
69
  `;
69
- // Write enums (ITEM_KINDS, SPEC_TARGETS, REALIZE_TARGETS, DECISION_KINDS, OUTCOMES, GATES, DISPOSITIONS,
70
+ // Write enums (ITEM_KINDS, SPEC_TARGETS, REALIZE_TARGETS, DECISION_KINDS, GATES, DISPOSITIONS,
70
71
  // BLOCKER_KINDS, RESOLUTION_RULES, EVIDENCE_KINDS, RESULTS) are sourced from the ingest contract — the
71
72
  // SINGLE source, so the CLI's `oneOf` checks and the WorkEvent mapper cannot diverge on accepted values.
72
73
  // (`linked-accepted` openness is DERIVED at report/query time vs `--commit`, v2.2a hybrid-A; see
@@ -74,6 +75,7 @@ const USAGE = `usage: track <command>
74
75
  const REALIZATIONS = ['to-do', 'in-progress', 'done', 'cancelled', 'rejected'];
75
76
  const FROM_FORMATS = ['junit', 'json'];
76
77
  const BUCKETS_ARG = ['AWAITED', 'DROPPED', 'DONE', 'TO-DO'];
78
+ const DECISION_OUTCOMES = ['pending', 'go', 'no-go', 'deferred'];
77
79
  // `n/a` is decision-only; `query` projects non-decision rows, so it would never match.
78
80
  const ACCEPTANCES = ['fail', 'waived', 'unknown', 'stale', 'pass'];
79
81
  // The DossierArtifact discriminator (M5 §3.1). CLI-local: the union SHAPE is validated fail-closed in the
@@ -241,6 +243,19 @@ function num(flags, key) {
241
243
  throw new DomainError(`--${key} must be a number`);
242
244
  return n;
243
245
  }
246
+ function dossierOptions(raw) {
247
+ let parsed;
248
+ try {
249
+ parsed = JSON.parse(raw);
250
+ }
251
+ catch {
252
+ throw new DomainError('--options-json must be a JSON array of existing Option objects');
253
+ }
254
+ if (!Array.isArray(parsed)) {
255
+ throw new DomainError('--options-json must be a JSON array of existing Option objects');
256
+ }
257
+ return parsed;
258
+ }
244
259
  /** Validate a positional/flag against an allowed enum (CLI-boundary input validation). */
245
260
  function oneOf(value, allowed, name) {
246
261
  if (value === undefined || !allowed.includes(value)) {
@@ -301,6 +316,19 @@ export function runCli(rawArgv, io) {
301
316
  ...(trackDirEnv !== undefined ? { env: trackDirEnv } : {}),
302
317
  };
303
318
  try {
319
+ // `decision ls` is an inspection surface, not a write verb. Route it through the same serve-empty
320
+ // read path as report/query so it never creates a sidecar and can be used by a fresh reporting agent.
321
+ if (cmd === 'decision' && rest[0] === 'ls') {
322
+ const trackDir = resolveTrackDirOrNull(resolveOpts);
323
+ if (trackDir === null) {
324
+ io.err(`track: no .track resolved from ${io.cwd}. Run \`track init\` to create one ` +
325
+ `(the ONLY command that does), or pass --track-dir / TRACK_DIR. Serving an empty view.\n`);
326
+ }
327
+ return cmdDecisionLs(rest, {
328
+ io,
329
+ eventsPath: trackDir !== null ? eventsPathOf(trackDir) : eventsPathOf(join(resolve(io.cwd), '.track')),
330
+ });
331
+ }
304
332
  switch (cmd) {
305
333
  case '--version':
306
334
  case '-v':
@@ -659,7 +687,12 @@ function cmdDecision(args, ctx) {
659
687
  title: req(flags, 'title'),
660
688
  workspace: req(flags, 'workspace'),
661
689
  targets: req(flags, 'targets').split(',').map((s) => s.trim()).filter(Boolean),
662
- dossier: { context: opt(flags, 'context') ?? '', options: [], qa: [] },
690
+ dossier: {
691
+ context: req(flags, 'context'),
692
+ options: dossierOptions(req(flags, 'options-json')),
693
+ qa: [],
694
+ recommendation: { optionId: req(flags, 'recommendation'), rationale: req(flags, 'rationale') },
695
+ },
663
696
  ...(opt(flags, 'accountable') !== undefined ? { accountable: req(flags, 'accountable') } : {}),
664
697
  ...(opt(flags, 'engagement-ref') !== undefined ? { engagementRef: req(flags, 'engagement-ref') } : {}),
665
698
  });
@@ -667,16 +700,37 @@ function cmdDecision(args, ctx) {
667
700
  return 0;
668
701
  }
669
702
  if (sub === 'outcome') {
670
- track.setOutcome(positional[0], oneOf(positional[1], OUTCOMES, 'outcome'));
703
+ track.setOutcome(positional[0], oneOf(positional[1], ['deferred'], 'outcome'));
671
704
  io.out('ok\n');
672
705
  return 0;
673
706
  }
674
707
  if (sub === 'dossier') {
675
- // merge: a context-only edit must not erase existing options/qa/recommendation
708
+ // Merge: a context-only edit preserves a structured dossier. A legacy dossier must provide the
709
+ // full options/recommendation triplet here; the facade validates it fail-closed.
676
710
  const current = track.state().decisions.get(positional[0])?.dossier;
677
711
  if (current === undefined)
678
712
  throw new DomainError(`unknown decision ${positional[0]}`);
679
- track.reviseDossier(positional[0], { ...current, context: opt(flags, 'context') ?? current.context });
713
+ const optionJson = opt(flags, 'options-json');
714
+ const recommendation = opt(flags, 'recommendation');
715
+ const rationale = opt(flags, 'rationale');
716
+ if ((recommendation === undefined) !== (rationale === undefined)) {
717
+ throw new DomainError('--recommendation and --rationale must be supplied together');
718
+ }
719
+ if (optionJson !== undefined && recommendation === undefined && current.recommendation === undefined) {
720
+ throw new DomainError('migrating a legacy dossier requires --recommendation and --rationale');
721
+ }
722
+ track.reviseDossier(positional[0], {
723
+ ...current,
724
+ context: opt(flags, 'context') ?? current.context,
725
+ ...(optionJson !== undefined ? { options: dossierOptions(optionJson) } : {}),
726
+ ...(recommendation !== undefined ? { recommendation: { optionId: recommendation, rationale: rationale } } : {}),
727
+ });
728
+ io.out('ok\n');
729
+ return 0;
730
+ }
731
+ if (sub === 'select') {
732
+ const outcome = opt(flags, 'outcome');
733
+ track.selectDecisionOption(positional[0], positional[1], outcome === undefined ? 'go' : oneOf(outcome, ['go', 'no-go'], '--outcome'));
680
734
  io.out('ok\n');
681
735
  return 0;
682
736
  }
@@ -719,7 +773,7 @@ function cmdDecision(args, ctx) {
719
773
  io.out('ok\n');
720
774
  return 0;
721
775
  }
722
- io.err('usage: track decision <new|outcome|dossier|disposition|add-artifact>\n');
776
+ io.err('usage: track decision <new|outcome|select|dossier|disposition|add-artifact>\n');
723
777
  return 2;
724
778
  }
725
779
  function cmdBlocker(args, ctx) {
@@ -864,54 +918,41 @@ function cmdReport(args, ctx) {
864
918
  assertOnlyFlags(flags, ['raw', 'commit', 'require-accepted', 'format']);
865
919
  return emitSnapshot(flags, ctx, true);
866
920
  }
867
- // Reads go through the shared TrackReader command layer (same path the MCP server uses).
868
- const reader = new TrackReader(ctx.eventsPath);
869
921
  // Scope §A/§B — `--level <spec|plan|wp|lot|task>` switches to the status(level) projection.
870
- // Otherwise 0.19.1 prefers the WP/table conductor view; `--flat` is the deprecated legacy opt-out.
922
+ // Otherwise the WP/table conductor is the default; `--flat` is the legacy opt-out.
871
923
  if (opt(flags, 'level') !== undefined) {
872
924
  assertOnlyFlags(flags, ['level', 'commit', 'require-accepted', 'format']);
873
925
  assertBooleanFlag(flags, 'require-accepted');
874
- io.out(statusText(reader, oneOf(req(flags, 'level'), STATUS_LEVELS, '--level'), {
926
+ io.out(statusText(new TrackReader(ctx.eventsPath), oneOf(req(flags, 'level'), STATUS_LEVELS, '--level'), {
875
927
  baselineCommit: resolveCommit(io.cwd, opt(flags, 'commit')),
876
928
  requireAccepted: assertBooleanFlag(flags, 'require-accepted'),
877
929
  }, fmt(flags)));
878
930
  return 0;
879
931
  }
880
- const rawFormat = opt(flags, 'format');
881
- const widthArg = opt(flags, 'width');
882
- const inlineFlag = assertBooleanFlag(flags, 'inline');
883
- const inline = inlineFlag || widthArg !== undefined;
884
- if (inline && rawFormat !== undefined && rawFormat !== 'text') {
885
- 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
932
  assertOnlyFlags(flags, [
903
933
  'commit', 'require-accepted', 'decisions', 'active-roster', 'wp', 'flat', 'inline', 'width', 'format',
904
934
  ]);
905
- if (rawFormat !== undefined && !['text', 'md', 'html'].includes(rawFormat)) {
935
+ const rawFormat = opt(flags, 'format');
936
+ if (rawFormat !== undefined && !['json', 'text', 'md', 'html'].includes(rawFormat)) {
906
937
  throw new DomainError('--format must be one of: json|text|md|html');
907
938
  }
939
+ const widthArg = opt(flags, 'width');
940
+ const inlineFlag = assertBooleanFlag(flags, 'inline');
941
+ const inline = inlineFlag || widthArg !== undefined;
942
+ const format = oneOf(rawFormat ?? 'text', ['json', 'text', 'md', 'html'], '--format');
943
+ if (inline && format !== 'text')
944
+ throw new DomainError('--inline/--width accepts no --format, or --format text');
908
945
  const requireAccepted = assertBooleanFlag(flags, 'require-accepted');
909
- const decisions = assertBooleanFlag(flags, 'decisions');
946
+ const requestedDecisions = assertBooleanFlag(flags, 'decisions');
910
947
  const activeRoster = assertBooleanFlag(flags, 'active-roster');
911
948
  const wp = assertBooleanFlag(flags, 'wp');
912
949
  const flat = assertBooleanFlag(flags, 'flat');
913
950
  if (wp && flat)
914
951
  throw new DomainError('--wp and --flat are mutually exclusive');
952
+ if (format === 'json' && flat)
953
+ throw new DomainError('--flat is only meaningful for text or md reports');
954
+ if (format === 'html' && flat)
955
+ throw new DomainError('--format html is always the deterministic conductor and rejects --flat');
915
956
  let width;
916
957
  if (widthArg !== undefined) {
917
958
  if (!/^\d+$/u.test(widthArg))
@@ -920,28 +961,30 @@ function cmdReport(args, ctx) {
920
961
  if (width < 40 || width > 240)
921
962
  throw new DomainError('--width must be an integer in [40,240]');
922
963
  }
923
- const baselineInput = opt(flags, 'commit') ?? 'HEAD';
924
- const baselineCommit = resolveCommit(io.cwd, baselineInput);
925
- const format = inline
926
- ? 'inline'
927
- : rawFormat === 'md' || rawFormat === 'html'
928
- ? rawFormat
929
- : 'text';
930
- io.out(generateAiReport({
931
- reader,
932
- cwd: io.cwd,
933
- request: {
934
- baselineInput,
935
- baselineCommit,
936
- format,
937
- emphasis: wp ? 'workpackages' : flat ? 'flat' : 'default',
938
- requireAccepted,
939
- decisionEmphasis: decisions ? 'all' : 'open-only',
940
- activeRoster,
941
- },
942
- ...(width !== undefined ? { width } : {}),
943
- ...(io.env !== undefined ? { env: io.env } : {}),
944
- }).output);
964
+ // Every format is a deterministic read over the folded log. The default human view is
965
+ // the conductor; --flat explicitly requests the legacy bucket projection. JSON preserves
966
+ // its established flat machine contract unless --wp is explicit, so --flat is rejected there
967
+ // instead of being silently accepted as a no-op. Contextual prose remains advisory agent work;
968
+ // no report format invokes an adapter, gateway, subprocess, or model.
969
+ const options = {
970
+ baselineCommit: resolveCommit(io.cwd, opt(flags, 'commit')),
971
+ requireAccepted,
972
+ // The owner-facing conductor always classifies decision dossiers. JSON keeps its
973
+ // established opt-in decision payload, while --flat retains the legacy opt-in.
974
+ decisions: requestedDecisions || (!flat && format !== 'json'),
975
+ wpTree: wp || (!flat && format !== 'json'),
976
+ activeRoster,
977
+ };
978
+ const reader = new TrackReader(ctx.eventsPath);
979
+ if (inline) {
980
+ io.out(reportInline(reader, options, width === undefined ? {} : { width }));
981
+ }
982
+ else if (format === 'html') {
983
+ io.out(reportHtml(reader, options));
984
+ }
985
+ else {
986
+ io.out(reportText(reader, options, format));
987
+ }
945
988
  return 0;
946
989
  }
947
990
  function emitSnapshot(flags, ctx, allowRaw) {
@@ -996,6 +1039,43 @@ function cmdQuery(args, ctx) {
996
1039
  }, { baselineCommit: resolveCommit(io.cwd, opt(flags, 'commit')) }, fmt(flags)));
997
1040
  return 0;
998
1041
  }
1042
+ /**
1043
+ * List decision dossiers without a renderer cap. `structure` makes the prose-only migration state explicit;
1044
+ * only a structured dossier has durable alternatives + a durable recommendation to render as a choice.
1045
+ */
1046
+ function cmdDecisionLs(args, ctx) {
1047
+ const { io } = ctx;
1048
+ const { positional, flags } = parseFlags(args.slice(1));
1049
+ if (positional.length > 0)
1050
+ throw new DomainError(`unexpected decision ls argument(s): ${positional.join(' ')}`);
1051
+ assertOnlyFlags(flags, ['workspace', 'outcome', 'format', 'commit']);
1052
+ for (const name of ['workspace', 'outcome', 'format', 'commit'])
1053
+ assertValueFlag(flags, name);
1054
+ const format = fmt(flags);
1055
+ const baselineCommit = resolveCommit(io.cwd, opt(flags, 'commit'));
1056
+ const reader = new TrackReader(ctx.eventsPath);
1057
+ const workspace = opt(flags, 'workspace');
1058
+ const outcome = opt(flags, 'outcome') !== undefined ? oneOf(req(flags, 'outcome'), DECISION_OUTCOMES, '--outcome') : undefined;
1059
+ const rows = reader.decisionDossiers({ baselineCommit })
1060
+ .filter((decision) => (workspace === undefined || decision.workspace === workspace) && (outcome === undefined || decision.outcome === outcome))
1061
+ .map(({ dossier, ...decision }) => ({
1062
+ ...decision,
1063
+ options: dossier.options,
1064
+ ...(dossier.recommendation !== undefined ? { recommendation: dossier.recommendation } : {}),
1065
+ }));
1066
+ if (format === 'json') {
1067
+ io.out(`${JSON.stringify(rows, null, 2)}\n`);
1068
+ return 0;
1069
+ }
1070
+ const lines = rows.map((decision) => {
1071
+ const safe = (value) => displayText(value, format);
1072
+ const recommendation = safe(decision.recommendation?.optionId ?? '-');
1073
+ const line = `${safe(decision.id)} · ${safe(decision.workspace)} · ${safe(decision.outcome)} · ${safe(decision.structure ?? 'unstructured')} · options:${decision.options.length} · recommendation:${recommendation} — ${safe(decision.title)}`;
1074
+ return format === 'md' ? `- ${line}` : line;
1075
+ });
1076
+ io.out(lines.length > 0 ? `${lines.join('\n')}\n` : '');
1077
+ return 0;
1078
+ }
999
1079
  /**
1000
1080
  * `track workspace-activity --workspace <id> [--baseline-commit <sha>] [--now <iso>] [--idle-ms <ms>]
1001
1081
  * [--format json|text]` — a poll surface over the shipped, CLOCKLESS `TrackReader.workspaceActivity`
@@ -1079,6 +1159,15 @@ async function cmdFocus(args, ctx, _noStore) {
1079
1159
  const { io } = ctx;
1080
1160
  const FOCUS_USAGE = 'usage: track focus <decision-id> --workspace <w> [--format terminal|md|html] [--baseline-commit <sha>]\n';
1081
1161
  const { positional, flags } = parseFlags(args);
1162
+ try {
1163
+ assertOnlyFlags(flags, ['workspace', 'format', 'baseline-commit']);
1164
+ for (const name of ['workspace', 'format', 'baseline-commit'])
1165
+ assertValueFlag(flags, name);
1166
+ }
1167
+ catch (error) {
1168
+ io.err(`error: ${error instanceof Error ? error.message : String(error)}\n`);
1169
+ return 2;
1170
+ }
1082
1171
  // decision-id is positional + REQUIRED; --workspace is a REQUIRED flag (both gate at the CLI boundary
1083
1172
  // with rc=2 + usage, never reaching focus). Validate BEFORE the dynamic import so a usage error is
1084
1173
  // independent of whether focus is installed.
@@ -1098,6 +1187,15 @@ async function cmdFocus(args, ctx, _noStore) {
1098
1187
  io.err(`error: ${error instanceof Error ? error.message : String(error)}\n`);
1099
1188
  return 2;
1100
1189
  }
1190
+ const baselineCommit = resolveCommit(io.cwd, opt(flags, 'baseline-commit'));
1191
+ const decision = new TrackReader(ctx.eventsPath)
1192
+ .report({ baselineCommit, decisions: true })
1193
+ .decisions
1194
+ ?.find((candidate) => candidate.id === decisionId);
1195
+ if (decision !== undefined && decision.workspace !== workspace) {
1196
+ io.err(`error: decision ${decisionId} belongs to workspace ${decision.workspace}, not ${workspace}\n`);
1197
+ return 3;
1198
+ }
1101
1199
  // Load the focus render binding + core. focus is an integrated dependency → a MODULE_NOT_FOUND means the
1102
1200
  // integrated focus renderer is unavailable: map it to rc=1 + a helpful hint (NOT a stack trace).
1103
1201
  let focusTrack;
@@ -1118,7 +1216,7 @@ async function cmdFocus(args, ctx, _noStore) {
1118
1216
  try {
1119
1217
  // PURE / read-only / clockless: `readAt` is supplied at the CLI boundary (track holds no clock), the
1120
1218
  // 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: resolveCommit(io.cwd, opt(flags, 'baseline-commit')), decisionId }, new Date().toISOString());
1219
+ const doc = focusTrack.readDecisionDossier(ctx.eventsPath, { workspace, baselineCommit, decisionId }, new Date().toISOString());
1122
1220
  const rendered = format === 'md' ? core.renderMd(doc) : format === 'html' ? core.renderHtml(doc, HTML_HOOKS) : core.renderTerminal(doc);
1123
1221
  io.out(rendered.endsWith('\n') ? rendered : `${rendered}\n`);
1124
1222
  return 0;