@deftai/directive-core 0.90.0 → 0.92.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 (101) hide show
  1. package/dist/cache/fetch.js +3 -2
  2. package/dist/cache/io.d.ts +13 -2
  3. package/dist/cache/io.js +36 -7
  4. package/dist/cache/operations.js +5 -3
  5. package/dist/doctor/main.js +37 -0
  6. package/dist/doctor/openclaw-skills.d.ts +42 -8
  7. package/dist/doctor/openclaw-skills.js +281 -38
  8. package/dist/eval/health.d.ts +10 -1
  9. package/dist/eval/health.js +16 -16
  10. package/dist/hooks/dispatcher.d.ts +6 -1
  11. package/dist/hooks/dispatcher.js +30 -0
  12. package/dist/init-deposit/gitignore.js +3 -0
  13. package/dist/init-deposit/hygiene.d.ts +43 -1
  14. package/dist/init-deposit/hygiene.js +112 -7
  15. package/dist/init-deposit/init-deposit.d.ts +5 -0
  16. package/dist/init-deposit/init-deposit.js +37 -2
  17. package/dist/init-deposit/scaffold.js +3 -1
  18. package/dist/intake/github-auth-modes.d.ts +1 -1
  19. package/dist/intake/github-auth-modes.js +25 -3
  20. package/dist/intake/issue-ingest-cli.js +13 -0
  21. package/dist/intake/reconcile-issues-cli.js +13 -0
  22. package/dist/orchestration/probe-session.d.ts +5 -1
  23. package/dist/orchestration/probe-session.js +24 -13
  24. package/dist/platform/agents-md.js +1 -1
  25. package/dist/policy/deft-directive-disable.d.ts +86 -0
  26. package/dist/policy/deft-directive-disable.js +167 -0
  27. package/dist/policy/delivery-branch.d.ts +33 -0
  28. package/dist/policy/delivery-branch.js +124 -0
  29. package/dist/policy/index.d.ts +2 -0
  30. package/dist/policy/index.js +17 -1
  31. package/dist/policy/plan-extensions.d.ts +10 -0
  32. package/dist/policy/plan-extensions.js +16 -0
  33. package/dist/pr-closing-keywords/detect.d.ts +13 -0
  34. package/dist/pr-closing-keywords/detect.js +73 -8
  35. package/dist/pr-closing-keywords/index.d.ts +2 -2
  36. package/dist/pr-closing-keywords/index.js +1 -1
  37. package/dist/pr-closing-keywords/main.js +119 -90
  38. package/dist/pr-closing-keywords/types.d.ts +10 -0
  39. package/dist/render/constants.d.ts +16 -0
  40. package/dist/render/constants.js +16 -1
  41. package/dist/render/export-spec.d.ts +7 -1
  42. package/dist/render/export-spec.js +51 -5
  43. package/dist/render/index.d.ts +3 -2
  44. package/dist/render/index.js +2 -2
  45. package/dist/render/roadmap-render.js +164 -60
  46. package/dist/render/scope-outlook.d.ts +5 -0
  47. package/dist/render/scope-outlook.js +3 -0
  48. package/dist/render/spec-render.d.ts +28 -2
  49. package/dist/render/spec-render.js +111 -14
  50. package/dist/render/text-utils.d.ts +6 -0
  51. package/dist/render/text-utils.js +23 -0
  52. package/dist/scm/binary.d.ts +3 -0
  53. package/dist/scm/binary.js +10 -2
  54. package/dist/scm/call.d.ts +5 -0
  55. package/dist/scm/call.js +5 -0
  56. package/dist/scm/index.d.ts +1 -0
  57. package/dist/scm/index.js +1 -0
  58. package/dist/scm/main.d.ts +5 -0
  59. package/dist/scm/main.js +27 -0
  60. package/dist/scm/readiness-cli.d.ts +24 -0
  61. package/dist/scm/readiness-cli.js +85 -0
  62. package/dist/scm/readiness.d.ts +104 -0
  63. package/dist/scm/readiness.js +421 -0
  64. package/dist/scope/batch-promote.d.ts +26 -0
  65. package/dist/scope/batch-promote.js +167 -0
  66. package/dist/scope/brief-io.d.ts +3 -1
  67. package/dist/scope/brief-io.js +5 -3
  68. package/dist/scope/delivery-evidence.d.ts +112 -0
  69. package/dist/scope/delivery-evidence.js +419 -0
  70. package/dist/scope/index.d.ts +2 -0
  71. package/dist/scope/index.js +2 -0
  72. package/dist/scope/main.d.ts +7 -0
  73. package/dist/scope/main.js +155 -9
  74. package/dist/scope/registry-artifact-sync.js +4 -1
  75. package/dist/scope/transition.d.ts +11 -1
  76. package/dist/scope/transition.js +30 -4
  77. package/dist/scope/wip-cap-check.d.ts +6 -0
  78. package/dist/scope/wip-cap-check.js +14 -2
  79. package/dist/session/ritual-sentinel.js +21 -12
  80. package/dist/session/session-start-hook.d.ts +3 -0
  81. package/dist/session/session-start-hook.js +21 -0
  82. package/dist/session/session-start.d.ts +7 -0
  83. package/dist/session/session-start.js +85 -0
  84. package/dist/swarm/complete-cohort.d.ts +20 -1
  85. package/dist/swarm/complete-cohort.js +56 -9
  86. package/dist/swarm/finalize-cohort-cli.js +9 -0
  87. package/dist/swarm/finalize-cohort.d.ts +8 -0
  88. package/dist/swarm/finalize-cohort.js +217 -21
  89. package/dist/triage/welcome/index.d.ts +1 -1
  90. package/dist/triage/welcome/index.js +1 -1
  91. package/dist/triage/welcome/onboard.js +7 -1
  92. package/dist/triage/welcome/prior-state.d.ts +7 -0
  93. package/dist/triage/welcome/prior-state.js +16 -2
  94. package/dist/triage/welcome/writers.d.ts +10 -0
  95. package/dist/triage/welcome/writers.js +72 -2
  96. package/dist/user-config/experimental-rules.d.ts +43 -0
  97. package/dist/user-config/experimental-rules.js +162 -0
  98. package/dist/user-config/index.d.ts +1 -0
  99. package/dist/user-config/index.js +1 -0
  100. package/dist/verify-source/contained-writes.js +1 -1
  101. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
1
  import { EXIT_CONFIG_ERROR, EXIT_HITS_FOUND, EXIT_OK } from "./constants.js";
2
- import { findHits, renderHit } from "./detect.js";
2
+ import { findAllClosingKeywordHits, findHits, renderHit } from "./detect.js";
3
3
  import { defaultRunGh, fetchPrBody, fetchPrCommitMessages } from "./gh.js";
4
4
  import { readCommitsFile, readTextFile } from "./io.js";
5
5
  export function parseAllowList(values) {
@@ -11,43 +11,43 @@ export function parseAllowList(values) {
11
11
  continue;
12
12
  }
13
13
  if (!/^\d+$/.test(tok)) {
14
- throw new Error(`Invalid issue number in --allow-known-false-positives: ${JSON.stringify(tok)}`);
14
+ throw new Error(`Invalid issue number in --allow-known-false-positives / --allow-close: ${JSON.stringify(tok)}`);
15
15
  }
16
16
  out.add(Number(tok));
17
17
  }
18
18
  }
19
19
  return out;
20
20
  }
21
+ function emptyParsed(error) {
22
+ return {
23
+ pr: null,
24
+ bodyFile: null,
25
+ commitsFile: null,
26
+ repo: null,
27
+ allowKnownFalsePositives: [],
28
+ allowClose: [],
29
+ mode: "both",
30
+ error,
31
+ };
32
+ }
21
33
  export function parseArgs(argv) {
22
34
  let pr = null;
23
35
  let bodyFile = null;
24
36
  let commitsFile = null;
25
37
  let repo = null;
38
+ let mode = "both";
26
39
  const allowKnownFalsePositives = [];
40
+ const allowClose = [];
27
41
  for (let i = 0; i < argv.length; i += 1) {
28
42
  const arg = argv[i];
29
43
  if (arg === "--pr") {
30
44
  const value = argv[i + 1];
31
45
  if (value === undefined) {
32
- return {
33
- pr,
34
- bodyFile,
35
- commitsFile,
36
- repo,
37
- allowKnownFalsePositives,
38
- error: "argument --pr: expected one argument",
39
- };
46
+ return emptyParsed("argument --pr: expected one argument");
40
47
  }
41
48
  const n = Number(value);
42
49
  if (!Number.isInteger(n)) {
43
- return {
44
- pr,
45
- bodyFile,
46
- commitsFile,
47
- repo,
48
- allowKnownFalsePositives,
49
- error: `invalid int value: ${JSON.stringify(value)}`,
50
- };
50
+ return emptyParsed(`invalid int value: ${JSON.stringify(value)}`);
51
51
  }
52
52
  pr = n;
53
53
  i += 1;
@@ -56,28 +56,14 @@ export function parseArgs(argv) {
56
56
  const value = arg.slice("--pr=".length);
57
57
  const n = Number(value);
58
58
  if (!Number.isInteger(n)) {
59
- return {
60
- pr,
61
- bodyFile,
62
- commitsFile,
63
- repo,
64
- allowKnownFalsePositives,
65
- error: `invalid int value: ${JSON.stringify(value)}`,
66
- };
59
+ return emptyParsed(`invalid int value: ${JSON.stringify(value)}`);
67
60
  }
68
61
  pr = n;
69
62
  }
70
63
  else if (arg === "--body-file") {
71
64
  const value = argv[i + 1];
72
65
  if (value === undefined) {
73
- return {
74
- pr,
75
- bodyFile,
76
- commitsFile,
77
- repo,
78
- allowKnownFalsePositives,
79
- error: "argument --body-file: expected one argument",
80
- };
66
+ return emptyParsed("argument --body-file: expected one argument");
81
67
  }
82
68
  bodyFile = value;
83
69
  i += 1;
@@ -88,14 +74,7 @@ export function parseArgs(argv) {
88
74
  else if (arg === "--commits-file") {
89
75
  const value = argv[i + 1];
90
76
  if (value === undefined) {
91
- return {
92
- pr,
93
- bodyFile,
94
- commitsFile,
95
- repo,
96
- allowKnownFalsePositives,
97
- error: "argument --commits-file: expected one argument",
98
- };
77
+ return emptyParsed("argument --commits-file: expected one argument");
99
78
  }
100
79
  commitsFile = value;
101
80
  i += 1;
@@ -106,14 +85,7 @@ export function parseArgs(argv) {
106
85
  else if (arg === "--repo") {
107
86
  const value = argv[i + 1];
108
87
  if (value === undefined) {
109
- return {
110
- pr,
111
- bodyFile,
112
- commitsFile,
113
- repo,
114
- allowKnownFalsePositives,
115
- error: "argument --repo: expected one argument",
116
- };
88
+ return emptyParsed("argument --repo: expected one argument");
117
89
  }
118
90
  repo = value;
119
91
  i += 1;
@@ -121,17 +93,28 @@ export function parseArgs(argv) {
121
93
  else if (arg?.startsWith("--repo=")) {
122
94
  repo = arg.slice("--repo=".length);
123
95
  }
96
+ else if (arg === "--mode") {
97
+ const value = argv[i + 1];
98
+ if (value === undefined) {
99
+ return emptyParsed("argument --mode: expected one argument (fp|intent|both)");
100
+ }
101
+ if (value !== "fp" && value !== "intent" && value !== "both") {
102
+ return emptyParsed(`invalid --mode: ${JSON.stringify(value)} (expected fp|intent|both)`);
103
+ }
104
+ mode = value;
105
+ i += 1;
106
+ }
107
+ else if (arg?.startsWith("--mode=")) {
108
+ const value = arg.slice("--mode=".length);
109
+ if (value !== "fp" && value !== "intent" && value !== "both") {
110
+ return emptyParsed(`invalid --mode: ${JSON.stringify(value)} (expected fp|intent|both)`);
111
+ }
112
+ mode = value;
113
+ }
124
114
  else if (arg === "--allow-known-false-positives") {
125
115
  const value = argv[i + 1];
126
116
  if (value === undefined) {
127
- return {
128
- pr,
129
- bodyFile,
130
- commitsFile,
131
- repo,
132
- allowKnownFalsePositives,
133
- error: "argument --allow-known-false-positives: expected one argument",
134
- };
117
+ return emptyParsed("argument --allow-known-false-positives: expected one argument");
135
118
  }
136
119
  allowKnownFalsePositives.push(value);
137
120
  i += 1;
@@ -139,45 +122,69 @@ export function parseArgs(argv) {
139
122
  else if (arg?.startsWith("--allow-known-false-positives=")) {
140
123
  allowKnownFalsePositives.push(arg.slice("--allow-known-false-positives=".length));
141
124
  }
125
+ else if (arg === "--allow-close") {
126
+ const value = argv[i + 1];
127
+ if (value === undefined) {
128
+ return emptyParsed("argument --allow-close: expected one argument");
129
+ }
130
+ allowClose.push(value);
131
+ i += 1;
132
+ }
133
+ else if (arg?.startsWith("--allow-close=")) {
134
+ allowClose.push(arg.slice("--allow-close=".length));
135
+ }
142
136
  else if (arg?.startsWith("-")) {
143
- return {
144
- pr,
145
- bodyFile,
146
- commitsFile,
147
- repo,
148
- allowKnownFalsePositives,
149
- error: `unrecognized arguments: ${arg}`,
150
- };
137
+ return emptyParsed(`unrecognized arguments: ${arg}`);
151
138
  }
152
139
  else {
153
- return {
154
- pr,
155
- bodyFile,
156
- commitsFile,
157
- repo,
158
- allowKnownFalsePositives,
159
- error: `unrecognized arguments: ${arg}`,
160
- };
140
+ return emptyParsed(`unrecognized arguments: ${arg}`);
161
141
  }
162
142
  }
163
- return { pr, bodyFile, commitsFile, repo, allowKnownFalsePositives };
143
+ return { pr, bodyFile, commitsFile, repo, allowKnownFalsePositives, allowClose, mode };
164
144
  }
165
145
  function filterHits(hits, allowList) {
166
146
  return hits.filter((h) => !allowList.has(h.issueNumber));
167
147
  }
168
- function emitResult(hits, filtered) {
169
- if (filtered.length === 0) {
170
- if (hits.length > 0) {
171
- process.stderr.write(`OK: ${hits.length} hit(s) suppressed by --allow-known-false-positives.\n`);
148
+ function emitResult(mode, fpFiltered, intentFiltered, fpSuppressed, intentSuppressed) {
149
+ const totalFail = fpFiltered.length + intentFiltered.length;
150
+ if (totalFail === 0) {
151
+ const notes = [];
152
+ if (fpSuppressed > 0) {
153
+ notes.push(`${fpSuppressed} FP hit(s) suppressed by --allow-known-false-positives`);
172
154
  }
173
- else {
155
+ if (intentSuppressed > 0) {
156
+ notes.push(`${intentSuppressed} intent hit(s) suppressed by --allow-close`);
157
+ }
158
+ if (notes.length > 0) {
159
+ process.stderr.write(`OK: ${notes.join("; ")}.\n`);
160
+ }
161
+ else if (mode === "fp") {
174
162
  process.stderr.write("OK: no closing-keyword negation/quotation/example/code-block hits found.\n");
175
163
  }
164
+ else if (mode === "intent") {
165
+ process.stderr.write("OK: no unallowlisted closing-keyword hits (intent mode; #3015).\n");
166
+ }
167
+ else {
168
+ process.stderr.write("OK: no closing-keyword FP or unallowlisted intent hits found (#737 / #3015).\n");
169
+ }
176
170
  return EXIT_OK;
177
171
  }
178
- process.stderr.write(`FAIL: ${filtered.length} closing-keyword negation-context hit(s) found (see #737). Rewrite the PR body / commit messages to avoid the trigger token, or pass --allow-known-false-positives to suppress.\n`);
179
- for (const h of filtered) {
180
- process.stderr.write(`${renderHit(h)}\n`);
172
+ if (fpFiltered.length > 0) {
173
+ process.stderr.write(`FAIL: ${fpFiltered.length} closing-keyword negation-context hit(s) found (FP mode / #737). ` +
174
+ "Rewrite the PR body / commit messages to avoid the trigger token, or pass " +
175
+ "--allow-known-false-positives to suppress known-safe quotes.\n");
176
+ for (const h of fpFiltered) {
177
+ process.stderr.write(`${renderHit(h)}\n`);
178
+ }
179
+ }
180
+ if (intentFiltered.length > 0) {
181
+ process.stderr.write(`FAIL: ${intentFiltered.length} real closing-keyword hit(s) without allowlist (intent mode / #3015 class D). ` +
182
+ "Default PR bodies use Tracking: #N / Related: #N / Refs #N. Use Closes/Fixes/Resolves only when full " +
183
+ "issue DoD is met, then pass --allow-close <N,M> on the lint (body trailers are not an authorization path). " +
184
+ "Conditional prose (Phase A / only if / partial) does NOT prevent GitHub auto-close.\n");
185
+ for (const h of intentFiltered) {
186
+ process.stderr.write(`${renderHit(h)}\n`);
187
+ }
181
188
  }
182
189
  return EXIT_HITS_FOUND;
183
190
  }
@@ -187,9 +194,11 @@ export function run(argv, options = {}) {
187
194
  process.stderr.write(`Error: ${args.error}\n`);
188
195
  return EXIT_CONFIG_ERROR;
189
196
  }
190
- let allowList;
197
+ let fpAllow;
198
+ let closeAllow;
191
199
  try {
192
- allowList = parseAllowList(args.allowKnownFalsePositives);
200
+ fpAllow = parseAllowList(args.allowKnownFalsePositives);
201
+ closeAllow = parseAllowList(args.allowClose);
193
202
  }
194
203
  catch (exc) {
195
204
  const message = exc instanceof Error ? exc.message : String(exc);
@@ -230,15 +239,35 @@ export function run(argv, options = {}) {
230
239
  commitMessages = msgs;
231
240
  }
232
241
  }
233
- const hits = [];
242
+ const runFp = args.mode === "fp" || args.mode === "both";
243
+ const runIntent = args.mode === "intent" || args.mode === "both";
244
+ const fpHits = [];
245
+ const intentHits = [];
234
246
  if (bodyText !== null) {
235
- hits.push(...findHits(bodyText, "pr-body"));
247
+ if (runFp) {
248
+ fpHits.push(...findHits(bodyText, "pr-body"));
249
+ }
250
+ if (runIntent) {
251
+ // Intent = class D only (bare/conditional real closes). Class A FP-context
252
+ // hits (negation/quote/example/code) stay exclusive to FP mode so operators
253
+ // do not need --allow-close for quoted fixtures (#3015 / SLizard).
254
+ intentHits.push(...findAllClosingKeywordHits(bodyText, "pr-body").filter((h) => h.reason === "intent"));
255
+ }
236
256
  }
237
257
  for (let idx = 0; idx < commitMessages.length; idx += 1) {
238
- hits.push(...findHits(commitMessages[idx] ?? "", `commit:${idx}`));
258
+ const msg = commitMessages[idx] ?? "";
259
+ if (runFp) {
260
+ fpHits.push(...findHits(msg, `commit:${idx}`));
261
+ }
262
+ if (runIntent) {
263
+ intentHits.push(...findAllClosingKeywordHits(msg, `commit:${idx}`).filter((h) => h.reason === "intent"));
264
+ }
239
265
  }
240
- const filtered = filterHits(hits, allowList);
241
- return emitResult(hits, filtered);
266
+ // Intent allowlist is CLI --allow-close only (#3015). Body trailers are not
267
+ // an authorization path (Markdown example/fence false-authorization class).
268
+ const fpFiltered = filterHits(fpHits, fpAllow);
269
+ const intentFiltered = filterHits(intentHits, closeAllow);
270
+ return emitResult(args.mode, fpFiltered, intentFiltered, fpHits.length - fpFiltered.length, intentHits.length - intentFiltered.length);
242
271
  }
243
272
  export function cmdPrCheckClosingKeywords(argv, options = {}) {
244
273
  return run(argv, options);
@@ -12,12 +12,22 @@ export interface RunGhResult {
12
12
  }
13
13
  /** Injectable gh subprocess seam (#1366 / parity harness). */
14
14
  export type RunGhFn = (cmd: readonly string[]) => RunGhResult;
15
+ /** Lint mode for closing-keyword checks (#3015). */
16
+ export type ClosingKeywordMode = "fp" | "intent" | "both";
15
17
  export interface ParsedArgs {
16
18
  readonly pr: number | null;
17
19
  readonly bodyFile: string | null;
18
20
  readonly commitsFile: string | null;
19
21
  readonly repo: string | null;
20
22
  readonly allowKnownFalsePositives: readonly string[];
23
+ /** Intent-mode allowlist of issue numbers permitted to use real closing keywords (#3015). */
24
+ readonly allowClose: readonly string[];
25
+ /**
26
+ * fp = Layer 0 #737 (negation/quote/example/code only);
27
+ * intent = any closing keyword unless allowlisted (#3015 class D);
28
+ * both = default (FP + intent).
29
+ */
30
+ readonly mode: ClosingKeywordMode;
21
31
  readonly error?: string;
22
32
  }
23
33
  //# sourceMappingURL=types.d.ts.map
@@ -2,11 +2,27 @@
2
2
  export declare const VALID_STATUSES: Set<string>;
3
3
  export declare const VALID_VBRIEF_VERSIONS: Set<string>;
4
4
  export declare const SPECIFICATION_NARRATIVE_KEY_ORDER: readonly ["Overview", "ProblemStatement", "Goals", "UserStories", "Requirements", "SuccessMetrics", "EdgeCases", "Architecture", "TechDecisions", "ImplementationPhases", "PreImplementationGates"];
5
+ /** Migration capture narrative; omitted from default compact SPECIFICATION.md (#1566). */
6
+ export declare const LEGACY_ARTIFACTS_NARRATIVE_KEY = "LegacyArtifacts";
7
+ /**
8
+ * Default lifecycle scope aggregation for `task spec:render` / `renderSpec` (#1566).
9
+ * `off` = product narratives only; opt in with `current` (pending+active) or `all`.
10
+ */
11
+ export declare const DEFAULT_INCLUDE_SCOPES_MODE: "off";
12
+ /** Lifecycle scope aggregation modes for rendered specs (#1566). */
13
+ export type IncludeScopesMode = "off" | "current" | "all";
5
14
  export declare const LIFECYCLE_BUCKETS: ReadonlyArray<readonly [string, string]>;
6
15
  export declare const SPEC_RENDER_BANNER: string;
7
16
  export declare const PRD_NARRATIVE_KEY_ORDER: readonly ["Overview", "ProblemStatement", "Goals", "UserStories", "Requirements", "SuccessMetrics", "Architecture"];
8
17
  export declare const PRD_BANNER: string;
9
18
  export declare const PRD_GENERATED_SENTINEL = "AUTO-GENERATED by task prd:render";
19
+ /** Max completed scopes listed under ## Completed in ROADMAP.md (#2653). */
20
+ export declare const ROADMAP_COMPLETED_CAP = 25;
21
+ /**
22
+ * Shown when completed/ has history but pending/, active/, and proposed/ are empty.
23
+ * Prevents silent Completed-only output without documenting empty-forward (#2653 D2.2).
24
+ */
25
+ export declare const ROADMAP_EMPTY_FORWARD_MARKER: string;
10
26
  export declare const ROADMAP_BANNER: string;
11
27
  export declare const MIGRATOR_METADATA_KEY = "x-migrator";
12
28
  export declare const PROJECT_LIFECYCLE_FOLDERS: readonly ["proposed", "pending", "active", "completed", "cancelled"];
@@ -24,6 +24,13 @@ export const SPECIFICATION_NARRATIVE_KEY_ORDER = [
24
24
  "ImplementationPhases",
25
25
  "PreImplementationGates",
26
26
  ];
27
+ /** Migration capture narrative; omitted from default compact SPECIFICATION.md (#1566). */
28
+ export const LEGACY_ARTIFACTS_NARRATIVE_KEY = "LegacyArtifacts";
29
+ /**
30
+ * Default lifecycle scope aggregation for `task spec:render` / `renderSpec` (#1566).
31
+ * `off` = product narratives only; opt in with `current` (pending+active) or `all`.
32
+ */
33
+ export const DEFAULT_INCLUDE_SCOPES_MODE = "off";
27
34
  export const LIFECYCLE_BUCKETS = [
28
35
  ["pending", "Pending"],
29
36
  ["active", "Active"],
@@ -47,9 +54,17 @@ export const PRD_BANNER = "<!-- AUTO-GENERATED by task prd:render -- DO NOT EDIT
47
54
  "<!-- Source of truth: vbrief/specification.vbrief.json -->\n" +
48
55
  "<!-- Regenerate with: task prd:render -->\n";
49
56
  export const PRD_GENERATED_SENTINEL = "AUTO-GENERATED by task prd:render";
57
+ /** Max completed scopes listed under ## Completed in ROADMAP.md (#2653). */
58
+ export const ROADMAP_COMPLETED_CAP = 25;
59
+ /**
60
+ * Shown when completed/ has history but pending/, active/, and proposed/ are empty.
61
+ * Prevents silent Completed-only output without documenting empty-forward (#2653 D2.2).
62
+ */
63
+ export const ROADMAP_EMPTY_FORWARD_MARKER = "_No open work in `pending/`, `active/`, or `proposed/`. " +
64
+ "ROADMAP is an orientation view — use `task triage:queue` or plan-sequence for work selection._\n";
50
65
  export const ROADMAP_BANNER = "<!-- AUTO-GENERATED by task roadmap:render -- DO NOT EDIT MANUALLY -->\n" +
51
66
  "<!-- Purpose: rendered roadmap -->\n" +
52
- "<!-- Source of truth: vbrief/pending/ (scope vBRIEFs) -->\n" +
67
+ "<!-- Source of truth: xbrief|vbrief pending/ + proposed/ + active/ (forward); completed/ capped -->\n" +
53
68
  "<!-- Regenerate with: task roadmap:render -->\n";
54
69
  export const MIGRATOR_METADATA_KEY = "x-migrator";
55
70
  export const PROJECT_LIFECYCLE_FOLDERS = [
@@ -1,9 +1,15 @@
1
+ import { type IncludeScopesMode } from "./constants.js";
1
2
  export type ExportAudience = "stakeholder" | "internal";
2
3
  export interface ExportSpecOptions {
3
4
  readonly projectRoot?: string;
4
5
  readonly outPath?: string;
5
6
  readonly audience?: ExportAudience;
6
- readonly includeScopes?: boolean;
7
+ /**
8
+ * Lifecycle scope aggregation (#1566). Default `off` (compact).
9
+ * `current` = pending+active; `all` / true = include completed archive.
10
+ */
11
+ readonly includeScopes?: boolean | IncludeScopesMode;
12
+ readonly includeLegacyArtifacts?: boolean;
7
13
  readonly proposedLimit?: number;
8
14
  }
9
15
  export type ExportSpecResult = readonly [boolean, string];
@@ -2,8 +2,11 @@ import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { greenfieldOverviewNonEmpty, renderNarrativeSections, resolveExportNarratives, } from "../spec-authority/narratives.js";
4
4
  import { resolveSpecAuthority } from "../spec-authority/resolver.js";
5
+ import { LEGACY_ARTIFACTS_NARRATIVE_KEY } from "./constants.js";
5
6
  import { buildScopeOutlookSection } from "./scope-outlook.js";
7
+ import { normalizeIncludeScopesMode, tryParseIncludeScopesMode, tryParseOnOffFlag, } from "./spec-render.js";
6
8
  import { validateSpec } from "./spec-validate.js";
9
+ import { stripTrailingWhitespace } from "./text-utils.js";
7
10
  function loadPlanTitle(path, fallback) {
8
11
  try {
9
12
  const doc = JSON.parse(readFileSync(path, "utf8"));
@@ -17,12 +20,24 @@ function loadPlanTitle(path, fallback) {
17
20
  }
18
21
  return fallback;
19
22
  }
23
+ function filterLegacyArtifacts(narratives, includeLegacyArtifacts) {
24
+ if (includeLegacyArtifacts)
25
+ return narratives;
26
+ const filtered = {};
27
+ for (const [key, val] of Object.entries(narratives)) {
28
+ if (key === LEGACY_ARTIFACTS_NARRATIVE_KEY)
29
+ continue;
30
+ filtered[key] = val;
31
+ }
32
+ return filtered;
33
+ }
20
34
  /** Unified spec export (#2013 / #1502). */
21
35
  export function exportSpec(options = {}) {
22
36
  const projectRoot = resolve(options.projectRoot ?? process.cwd());
23
37
  const outPath = options.outPath ?? join(projectRoot, "SPECIFICATION.md");
24
38
  const audience = options.audience ?? "stakeholder";
25
- const includeScopes = options.includeScopes ?? true;
39
+ const includeScopesMode = normalizeIncludeScopesMode(options.includeScopes);
40
+ const includeLegacyArtifacts = options.includeLegacyArtifacts ?? false;
26
41
  const includeProposed = audience === "internal";
27
42
  const authority = resolveSpecAuthority(projectRoot);
28
43
  if (!authority) {
@@ -39,7 +54,7 @@ export function exportSpec(options = {}) {
39
54
  "⚠ PROJECT-DEFINITION.xbrief.json Overview narrative is empty (D3). Populate Overview before export.",
40
55
  ];
41
56
  }
42
- const narratives = resolveExportNarratives(authority);
57
+ const narratives = filterLegacyArtifacts(resolveExportNarratives(authority), includeLegacyArtifacts);
43
58
  const title = authority.kind === "full-spec" && authority.specPath
44
59
  ? loadPlanTitle(authority.specPath, "Specification")
45
60
  : loadPlanTitle(authority.projectDefPath, "Specification");
@@ -48,15 +63,16 @@ export function exportSpec(options = {}) {
48
63
  `# ${title}\n`,
49
64
  ...renderNarrativeSections(narratives),
50
65
  ];
51
- if (includeScopes) {
66
+ if (includeScopesMode !== "off") {
52
67
  const scopeLines = buildScopeOutlookSection(authority.vbriefDir, {
53
68
  includeProposed,
54
69
  proposedLimit: options.proposedLimit,
70
+ includeCompleted: includeScopesMode === "all",
55
71
  });
56
72
  if (scopeLines.length > 0)
57
73
  lines.push(...scopeLines);
58
74
  }
59
- writeFileSync(outPath, lines.join("\n"), "utf8");
75
+ writeFileSync(outPath, stripTrailingWhitespace(lines.join("\n")), "utf8");
60
76
  return [true, `✓ Exported spec to ${outPath}`];
61
77
  }
62
78
  export function parseExportSpecArgv(argv) {
@@ -75,7 +91,37 @@ export function parseExportSpecArgv(argv) {
75
91
  continue;
76
92
  }
77
93
  if (arg === "--no-scopes") {
78
- options.includeScopes = false;
94
+ options.includeScopes = "off";
95
+ continue;
96
+ }
97
+ if (arg === "--include-scopes") {
98
+ options.includeScopes = "all";
99
+ continue;
100
+ }
101
+ if (arg.startsWith("--include-scopes=")) {
102
+ const value = arg.split("=", 2)[1] ?? "";
103
+ const parsed = tryParseIncludeScopesMode(value);
104
+ if (parsed === undefined) {
105
+ errors.push(`Invalid --include-scopes=${value} (expected off|current|all|active|on|true|1|yes|false|0|no)`);
106
+ }
107
+ else {
108
+ options.includeScopes = parsed;
109
+ }
110
+ continue;
111
+ }
112
+ if (arg === "--include-legacy-artifacts") {
113
+ options.includeLegacyArtifacts = true;
114
+ continue;
115
+ }
116
+ if (arg.startsWith("--include-legacy-artifacts=")) {
117
+ const value = arg.split("=", 2)[1] ?? "";
118
+ const parsed = tryParseOnOffFlag(value);
119
+ if (parsed === undefined) {
120
+ errors.push(`Invalid --include-legacy-artifacts=${value} (expected on|off|true|false|1|0|yes|no)`);
121
+ }
122
+ else {
123
+ options.includeLegacyArtifacts = parsed;
124
+ }
79
125
  continue;
80
126
  }
81
127
  if (arg.startsWith("--")) {
@@ -10,9 +10,10 @@ export * as roadmapRender from "./roadmap-render.js";
10
10
  export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, renderRoadmapToBuffer, } from "./roadmap-render.js";
11
11
  export { main as ruleMapMain } from "./rule-map.js";
12
12
  export { aggregateScopeSection, buildScopeOutlookSection } from "./scope-outlook.js";
13
+ export type { RenderSpecOptions } from "./spec-render.js";
13
14
  export * as specRender from "./spec-render.js";
14
- export { main as specRenderMain, parseIncludeScopesFlag, renderSpec } from "./spec-render.js";
15
+ export { main as specRenderMain, normalizeIncludeScopesMode, parseIncludeScopesFlag, renderSpec, tryParseIncludeScopesMode, tryParseOnOffFlag, } from "./spec-render.js";
15
16
  export * as specValidate from "./spec-validate.js";
16
17
  export { main as specValidateMain, validateSpec } from "./spec-validate.js";
17
- export { parsePhaseNumber, phaseSortKey, splitCamel, splitWords } from "./text-utils.js";
18
+ export { parsePhaseNumber, phaseSortKey, splitCamel, splitWords, stripTrailingWhitespace, } from "./text-utils.js";
18
19
  //# sourceMappingURL=index.d.ts.map
@@ -11,8 +11,8 @@ export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRo
11
11
  export { main as ruleMapMain } from "./rule-map.js";
12
12
  export { aggregateScopeSection, buildScopeOutlookSection } from "./scope-outlook.js";
13
13
  export * as specRender from "./spec-render.js";
14
- export { main as specRenderMain, parseIncludeScopesFlag, renderSpec } from "./spec-render.js";
14
+ export { main as specRenderMain, normalizeIncludeScopesMode, parseIncludeScopesFlag, renderSpec, tryParseIncludeScopesMode, tryParseOnOffFlag, } from "./spec-render.js";
15
15
  export * as specValidate from "./spec-validate.js";
16
16
  export { main as specValidateMain, validateSpec } from "./spec-validate.js";
17
- export { parsePhaseNumber, phaseSortKey, splitCamel, splitWords } from "./text-utils.js";
17
+ export { parsePhaseNumber, phaseSortKey, splitCamel, splitWords, stripTrailingWhitespace, } from "./text-utils.js";
18
18
  //# sourceMappingURL=index.js.map