@deftai/directive-core 0.89.0 → 0.91.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 (100) hide show
  1. package/dist/doctor/constants.d.ts +1 -1
  2. package/dist/doctor/constants.js +2 -0
  3. package/dist/doctor/flags.js +21 -1
  4. package/dist/doctor/index.d.ts +1 -0
  5. package/dist/doctor/index.js +1 -0
  6. package/dist/doctor/main.js +22 -0
  7. package/dist/doctor/openclaw-skills.d.ts +143 -0
  8. package/dist/doctor/openclaw-skills.js +706 -0
  9. package/dist/doctor/types.d.ts +11 -0
  10. package/dist/eval/health.d.ts +10 -1
  11. package/dist/eval/health.js +16 -16
  12. package/dist/hooks/dispatcher.js +20 -5
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +1 -0
  15. package/dist/init-deposit/hygiene.js +3 -0
  16. package/dist/init-deposit/init-deposit.d.ts +5 -0
  17. package/dist/init-deposit/init-deposit.js +37 -2
  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/lifecycle/events.js +2 -0
  23. package/dist/lifecycle/index.d.ts +1 -0
  24. package/dist/lifecycle/index.js +1 -0
  25. package/dist/lifecycle/stats.d.ts +53 -0
  26. package/dist/lifecycle/stats.js +286 -0
  27. package/dist/policy/plan-extensions.d.ts +10 -0
  28. package/dist/policy/plan-extensions.js +16 -0
  29. package/dist/pr-closing-keywords/detect.d.ts +13 -0
  30. package/dist/pr-closing-keywords/detect.js +73 -8
  31. package/dist/pr-closing-keywords/index.d.ts +2 -2
  32. package/dist/pr-closing-keywords/index.js +1 -1
  33. package/dist/pr-closing-keywords/main.js +119 -90
  34. package/dist/pr-closing-keywords/types.d.ts +10 -0
  35. package/dist/release/spawn.js +13 -0
  36. package/dist/release-e2e/git-ops.d.ts +7 -0
  37. package/dist/release-e2e/git-ops.js +35 -2
  38. package/dist/render/constants.d.ts +16 -0
  39. package/dist/render/constants.js +16 -1
  40. package/dist/render/export-spec.d.ts +7 -1
  41. package/dist/render/export-spec.js +51 -5
  42. package/dist/render/index.d.ts +3 -2
  43. package/dist/render/index.js +2 -2
  44. package/dist/render/roadmap-render.js +164 -60
  45. package/dist/render/scope-outlook.d.ts +5 -0
  46. package/dist/render/scope-outlook.js +3 -0
  47. package/dist/render/spec-render.d.ts +28 -2
  48. package/dist/render/spec-render.js +111 -14
  49. package/dist/render/text-utils.d.ts +6 -0
  50. package/dist/render/text-utils.js +23 -0
  51. package/dist/scm/binary.d.ts +3 -0
  52. package/dist/scm/binary.js +10 -2
  53. package/dist/scm/call.d.ts +5 -0
  54. package/dist/scm/call.js +5 -0
  55. package/dist/scm/index.d.ts +1 -0
  56. package/dist/scm/index.js +1 -0
  57. package/dist/scm/main.d.ts +5 -0
  58. package/dist/scm/main.js +27 -0
  59. package/dist/scm/readiness-cli.d.ts +24 -0
  60. package/dist/scm/readiness-cli.js +85 -0
  61. package/dist/scm/readiness.d.ts +104 -0
  62. package/dist/scm/readiness.js +421 -0
  63. package/dist/scope/batch-promote.d.ts +26 -0
  64. package/dist/scope/batch-promote.js +167 -0
  65. package/dist/scope/index.d.ts +1 -0
  66. package/dist/scope/index.js +1 -0
  67. package/dist/scope/main.d.ts +2 -0
  68. package/dist/scope/main.js +58 -7
  69. package/dist/scope/wip-cap-check.d.ts +6 -0
  70. package/dist/scope/wip-cap-check.js +14 -2
  71. package/dist/session/index.d.ts +2 -0
  72. package/dist/session/index.js +2 -0
  73. package/dist/session/process-cost-constants.d.ts +9 -0
  74. package/dist/session/process-cost-constants.js +11 -0
  75. package/dist/session/process-cost.d.ts +53 -0
  76. package/dist/session/process-cost.js +82 -0
  77. package/dist/session/ritual-sentinel.d.ts +5 -0
  78. package/dist/session/ritual-sentinel.js +12 -1
  79. package/dist/session/session-ready.d.ts +67 -0
  80. package/dist/session/session-ready.js +264 -0
  81. package/dist/session/session-start.d.ts +63 -1
  82. package/dist/session/session-start.js +432 -24
  83. package/dist/session/verify-session-ritual.d.ts +8 -0
  84. package/dist/session/verify-session-ritual.js +95 -35
  85. package/dist/tool-events/classify.d.ts +20 -0
  86. package/dist/tool-events/classify.js +634 -0
  87. package/dist/tool-events/index.d.ts +10 -0
  88. package/dist/tool-events/index.js +10 -0
  89. package/dist/tool-events/summarize.d.ts +34 -0
  90. package/dist/tool-events/summarize.js +93 -0
  91. package/dist/tool-events/types.d.ts +52 -0
  92. package/dist/tool-events/types.js +13 -0
  93. package/dist/triage/welcome/index.d.ts +1 -1
  94. package/dist/triage/welcome/index.js +1 -1
  95. package/dist/triage/welcome/onboard.js +7 -1
  96. package/dist/triage/welcome/prior-state.d.ts +7 -0
  97. package/dist/triage/welcome/prior-state.js +16 -2
  98. package/dist/triage/welcome/writers.d.ts +10 -0
  99. package/dist/triage/welcome/writers.js +72 -2
  100. package/package.json +7 -3
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { assertWriteTargetSafe } from "../fs/projection-containment.js";
4
4
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
5
- import { MIGRATOR_METADATA_KEY, ROADMAP_BANNER } from "./constants.js";
5
+ import { MIGRATOR_METADATA_KEY, ROADMAP_BANNER, ROADMAP_COMPLETED_CAP, ROADMAP_EMPTY_FORWARD_MARKER, } from "./constants.js";
6
6
  import { phaseSortKey } from "./text-utils.js";
7
7
  function scopeMetadataRank(plan) {
8
8
  const metadata = plan.metadata;
@@ -246,16 +246,79 @@ function renderScopeItem(vbriefData) {
246
246
  function resolveCompletedDir(pendingDir, completedDir) {
247
247
  return completedDir ?? join(dirname(pendingDir), "completed");
248
248
  }
249
- /** Single render-to-buffer entry used by both write and --check (mirrors ``scripts/roadmap_render.generate_roadmap_content``). */
250
- export function renderRoadmapToBuffer(pendingDir, completedDir) {
251
- const vbriefs = loadVbriefs(pendingDir);
252
- const resolvedCompleted = resolveCompletedDir(pendingDir, completedDir);
253
- const completedVbriefs = loadVbriefs(resolvedCompleted);
254
- const lines = [ROADMAP_BANNER, "# Roadmap\n"];
255
- if (vbriefs.length === 0 && completedVbriefs.length === 0) {
256
- lines.push("No pending work items.\n");
257
- return `${lines.join("\n")}\n`;
249
+ function lifecycleSibling(pendingDir, bucket) {
250
+ return join(dirname(pendingDir), bucket);
251
+ }
252
+ function hasLifecycleArtifacts(dir) {
253
+ if (!existsSync(dir))
254
+ return false;
255
+ try {
256
+ return readdirSync(dir).some((n) => hasArtifactSuffix(n));
257
+ }
258
+ catch {
259
+ return false;
258
260
  }
261
+ }
262
+ /**
263
+ * Prefer completion-time stamps over creation-dated filenames so the cap
264
+ * keeps recently completed scopes (#2653 Greptile P1).
265
+ * Order: plan.metadata.completedAt → plan.updated → envelope updated → _source_file.
266
+ */
267
+ function completedRecencyKey(vbrief) {
268
+ const plan = (vbrief.plan ?? {});
269
+ const metadata = plan.metadata;
270
+ if (typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)) {
271
+ const completedAt = metadata.completedAt;
272
+ if (typeof completedAt === "string" && completedAt.trim())
273
+ return completedAt.trim();
274
+ }
275
+ if (typeof plan.updated === "string" && plan.updated.trim())
276
+ return plan.updated.trim();
277
+ for (const envelopeKey of ["xBRIEFInfo", "vBRIEFInfo"]) {
278
+ const env = vbrief[envelopeKey];
279
+ if (typeof env === "object" && env !== null && !Array.isArray(env)) {
280
+ const updated = env.updated;
281
+ if (typeof updated === "string" && updated.trim())
282
+ return updated.trim();
283
+ }
284
+ }
285
+ return String(vbrief._source_file ?? "");
286
+ }
287
+ /** Newest-first by completion recency, then cap. */
288
+ function takeCompletedCap(vbriefs, cap = ROADMAP_COMPLETED_CAP) {
289
+ const sorted = [...vbriefs].sort((a, b) => {
290
+ const kb = completedRecencyKey(b);
291
+ const ka = completedRecencyKey(a);
292
+ const byKey = kb.localeCompare(ka);
293
+ if (byKey !== 0)
294
+ return byKey;
295
+ return String(b._source_file ?? "").localeCompare(String(a._source_file ?? ""));
296
+ });
297
+ if (sorted.length <= cap) {
298
+ return { shown: sorted, total: sorted.length, omitted: 0 };
299
+ }
300
+ return {
301
+ shown: sorted.slice(0, cap),
302
+ total: sorted.length,
303
+ omitted: sorted.length - cap,
304
+ };
305
+ }
306
+ /** Flat list of scope bullets (proposed / active / completed). */
307
+ function renderScopeList(vbriefs) {
308
+ const lines = [];
309
+ for (const vb of vbriefs)
310
+ lines.push(...renderScopeItem(vb));
311
+ if (vbriefs.length > 0)
312
+ lines.push("");
313
+ return lines;
314
+ }
315
+ /**
316
+ * Pending-only body: preserve phase-grouped and hierarchical heading models
317
+ * so existing ROADMAP consumers keep stable ## structure when pending is the
318
+ * sole forward source.
319
+ */
320
+ function renderPendingBody(vbriefs) {
321
+ const lines = [];
259
322
  const hasPhaseNarratives = vbriefs.some((vb) => {
260
323
  const plan = (vb.plan ?? {});
261
324
  return Boolean(migratorField(plan, "Phase"));
@@ -292,57 +355,94 @@ export function renderRoadmapToBuffer(pendingDir, completedDir) {
292
355
  lines.push("");
293
356
  }
294
357
  }
358
+ return lines;
295
359
  }
296
- else {
297
- for (const vbrief of vbriefs) {
298
- const plan = (vbrief.plan ?? {});
299
- const planTitle = String(plan.title ?? "Untitled");
300
- const issueRefs = extractIssueRefs(plan.references);
301
- const titleParts = [`## ${planTitle}`];
302
- if (issueRefs.length > 0)
303
- titleParts.push(`(${issueRefs.join(", ")})`);
304
- lines.push(`${titleParts.join(" ")}\n`);
305
- const narratives = plan.narratives;
306
- if (typeof narratives === "object" && narratives !== null && !Array.isArray(narratives)) {
307
- const overview = narratives.Overview;
308
- if (typeof overview === "string" && overview)
309
- lines.push(`${overview}\n`);
310
- }
311
- const depMap = buildEdgeMap(vbrief);
312
- const phases = Array.isArray(plan.items)
313
- ? plan.items.filter((p) => typeof p === "object" && p !== null && !Array.isArray(p))
314
- : [];
315
- const sortedPhases = topoSortItems(phases, depMap);
316
- for (const phase of sortedPhases) {
317
- const phaseId = String(phase.id ?? "");
318
- const phaseTitle = String(phase.title ?? "Untitled Phase");
319
- const phaseStatus = String(phase.status ?? "");
320
- let heading = phaseId ? `### ${phaseId}: ${phaseTitle}` : `### ${phaseTitle}`;
321
- if (phaseStatus)
322
- heading += ` \`[${phaseStatus}]\``;
323
- lines.push(`${heading}\n`);
324
- const narrative = phase.narrative;
325
- if (typeof narrative === "object" && narrative !== null && !Array.isArray(narrative)) {
326
- for (const [key, val] of Object.entries(narrative)) {
327
- if (key !== "Traces" && key !== "Acceptance")
328
- lines.push(`${String(val)}\n`);
329
- }
330
- }
331
- const subItems = phase.subItems;
332
- if (Array.isArray(subItems) && subItems.length > 0) {
333
- const subs = subItems.filter((s) => typeof s === "object" && s !== null && !Array.isArray(s));
334
- const sortedSubs = topoSortItems(subs, depMap);
335
- for (const item of sortedSubs)
336
- lines.push(...renderItem(item, depMap));
337
- lines.push("");
360
+ for (const vbrief of vbriefs) {
361
+ const plan = (vbrief.plan ?? {});
362
+ const planTitle = String(plan.title ?? "Untitled");
363
+ const issueRefs = extractIssueRefs(plan.references);
364
+ const titleParts = [`## ${planTitle}`];
365
+ if (issueRefs.length > 0)
366
+ titleParts.push(`(${issueRefs.join(", ")})`);
367
+ lines.push(`${titleParts.join(" ")}\n`);
368
+ const narratives = plan.narratives;
369
+ if (typeof narratives === "object" && narratives !== null && !Array.isArray(narratives)) {
370
+ const overview = narratives.Overview;
371
+ if (typeof overview === "string" && overview)
372
+ lines.push(`${overview}\n`);
373
+ }
374
+ const depMap = buildEdgeMap(vbrief);
375
+ const phases = Array.isArray(plan.items)
376
+ ? plan.items.filter((p) => typeof p === "object" && p !== null && !Array.isArray(p))
377
+ : [];
378
+ const sortedPhases = topoSortItems(phases, depMap);
379
+ for (const phase of sortedPhases) {
380
+ const phaseId = String(phase.id ?? "");
381
+ const phaseTitle = String(phase.title ?? "Untitled Phase");
382
+ const phaseStatus = String(phase.status ?? "");
383
+ let heading = phaseId ? `### ${phaseId}: ${phaseTitle}` : `### ${phaseTitle}`;
384
+ if (phaseStatus)
385
+ heading += ` \`[${phaseStatus}]\``;
386
+ lines.push(`${heading}\n`);
387
+ const narrative = phase.narrative;
388
+ if (typeof narrative === "object" && narrative !== null && !Array.isArray(narrative)) {
389
+ for (const [key, val] of Object.entries(narrative)) {
390
+ if (key !== "Traces" && key !== "Acceptance")
391
+ lines.push(`${String(val)}\n`);
338
392
  }
339
393
  }
340
- lines.push("---\n");
394
+ const subItems = phase.subItems;
395
+ if (Array.isArray(subItems) && subItems.length > 0) {
396
+ const subs = subItems.filter((s) => typeof s === "object" && s !== null && !Array.isArray(s));
397
+ const sortedSubs = topoSortItems(subs, depMap);
398
+ for (const item of sortedSubs)
399
+ lines.push(...renderItem(item, depMap));
400
+ lines.push("");
401
+ }
341
402
  }
403
+ lines.push("---\n");
404
+ }
405
+ return lines;
406
+ }
407
+ /** Single render-to-buffer entry used by both write and --check (mirrors ``scripts/roadmap_render.generate_roadmap_content``). */
408
+ export function renderRoadmapToBuffer(pendingDir, completedDir) {
409
+ const pendingVbriefs = loadVbriefs(pendingDir);
410
+ const activeVbriefs = loadVbriefs(lifecycleSibling(pendingDir, "active"));
411
+ const proposedVbriefs = loadVbriefs(lifecycleSibling(pendingDir, "proposed"));
412
+ const resolvedCompleted = resolveCompletedDir(pendingDir, completedDir);
413
+ const completedVbriefs = loadVbriefs(resolvedCompleted);
414
+ const lines = [ROADMAP_BANNER, "# Roadmap\n"];
415
+ const hasForward = pendingVbriefs.length > 0 || activeVbriefs.length > 0 || proposedVbriefs.length > 0;
416
+ const hasAny = hasForward || completedVbriefs.length > 0;
417
+ if (!hasAny) {
418
+ lines.push("No pending work items.\n");
419
+ return `${lines.join("\n")}\n`;
420
+ }
421
+ // D2.2: never emit Completed-only without an explicit empty-forward marker.
422
+ if (!hasForward && completedVbriefs.length > 0) {
423
+ lines.push("## Forward plan\n");
424
+ lines.push(ROADMAP_EMPTY_FORWARD_MARKER);
425
+ }
426
+ if (pendingVbriefs.length > 0) {
427
+ lines.push(...renderPendingBody(pendingVbriefs));
428
+ }
429
+ if (activeVbriefs.length > 0) {
430
+ lines.push("## Active\n");
431
+ lines.push(...renderScopeList(activeVbriefs));
432
+ }
433
+ if (proposedVbriefs.length > 0) {
434
+ lines.push("## Proposed\n");
435
+ lines.push("_Scopes not yet promoted to pending. Orientation only — not a substitute for `task triage:queue`._\n");
436
+ lines.push(...renderScopeList(proposedVbriefs));
342
437
  }
343
438
  if (completedVbriefs.length > 0) {
439
+ const { shown, total, omitted } = takeCompletedCap(completedVbriefs);
344
440
  lines.push("## Completed\n");
345
- for (const vb of completedVbriefs)
441
+ if (omitted > 0) {
442
+ lines.push(`_Showing ${shown.length} of ${total} completed scopes (newest first). ` +
443
+ `Full history: lifecycle \`completed/\` (or \`task report\` when available)._\n`);
444
+ }
445
+ for (const vb of shown)
346
446
  lines.push(...renderScopeItem(vb));
347
447
  lines.push("");
348
448
  }
@@ -378,19 +478,23 @@ export function renderRoadmap(pendingDir, outPath, completedDirOrOptions) {
378
478
  export function checkDrift(pendingDir, roadmapPath, completedDir) {
379
479
  const expected = renderRoadmapToBuffer(pendingDir, completedDir);
380
480
  if (!existsSync(roadmapPath)) {
381
- const hasPending = existsSync(pendingDir) && readdirSync(pendingDir).some((n) => hasArtifactSuffix(n));
481
+ const hasPending = hasLifecycleArtifacts(pendingDir);
482
+ const hasActive = hasLifecycleArtifacts(lifecycleSibling(pendingDir, "active"));
483
+ const hasProposed = hasLifecycleArtifacts(lifecycleSibling(pendingDir, "proposed"));
382
484
  const inferredCompleted = resolveCompletedDir(pendingDir, completedDir);
383
- const hasCompleted = existsSync(inferredCompleted) &&
384
- readdirSync(inferredCompleted).some((n) => hasArtifactSuffix(n));
385
- if (!hasPending && !hasCompleted) {
386
- return [true, "✓ No ROADMAP.md needed (no pending or completed vBRIEFs)"];
485
+ const hasCompleted = hasLifecycleArtifacts(inferredCompleted);
486
+ if (!hasPending && !hasActive && !hasProposed && !hasCompleted) {
487
+ return [true, "✓ No ROADMAP.md needed (no lifecycle scope vBRIEFs)"];
387
488
  }
388
489
  return [false, "✗ ROADMAP.md does not exist but vBRIEFs found"];
389
490
  }
390
491
  const actual = readFileSync(roadmapPath, "utf8");
391
492
  if (actual === expected)
392
493
  return [true, "✓ ROADMAP.md is up to date"];
393
- return [false, "✗ ROADMAP.md has drifted from pending/ vBRIEFs -- run: task roadmap:render"];
494
+ return [
495
+ false,
496
+ "✗ ROADMAP.md has drifted from lifecycle scope vBRIEFs -- run: task roadmap:render",
497
+ ];
394
498
  }
395
499
  /** CLI entry (mirrors ``scripts/roadmap_render.main``). */
396
500
  export function main(argv) {
@@ -1,6 +1,11 @@
1
1
  export interface ScopeOutlookOptions {
2
2
  readonly includeProposed?: boolean;
3
3
  readonly proposedLimit?: number;
4
+ /**
5
+ * When false, omit the completed lifecycle bucket (#1566 compact render).
6
+ * Default true so callers that opt into full aggregation keep prior behavior.
7
+ */
8
+ readonly includeCompleted?: boolean;
4
9
  }
5
10
  /** Build Scope outlook section per #2013 Wave 0 §4. */
6
11
  export declare function buildScopeOutlookSection(vbriefDir: string, options?: ScopeOutlookOptions): string[];
@@ -203,6 +203,7 @@ function filterProposed(vb) {
203
203
  export function buildScopeOutlookSection(vbriefDir, options = {}) {
204
204
  const includeProposed = options.includeProposed ?? false;
205
205
  const proposedLimit = options.proposedLimit ?? 0;
206
+ const includeCompleted = options.includeCompleted ?? true;
206
207
  const buckets = [];
207
208
  if (includeProposed) {
208
209
  let proposed = loadScopeVbriefs(join(vbriefDir, "proposed")).filter(([, vb]) => filterProposed(vb));
@@ -220,6 +221,8 @@ export function buildScopeOutlookSection(vbriefDir, options = {}) {
220
221
  }
221
222
  }
222
223
  for (const [folder, heading, filter] of COMMITTED_BUCKETS) {
224
+ if (folder === "completed" && !includeCompleted)
225
+ continue;
223
226
  let scopes = loadScopeVbriefs(join(vbriefDir, folder));
224
227
  if (filter)
225
228
  scopes = scopes.filter(([, vb]) => filter(vb));
@@ -1,12 +1,38 @@
1
+ import { type IncludeScopesMode } from "./constants.js";
2
+ /**
3
+ * Parse a string include-scopes token. Returns undefined when the token is not a known mode
4
+ * (callers MUST fail closed rather than silently omitting content — #1566 Greptile P1).
5
+ */
6
+ export declare function tryParseIncludeScopesMode(value: string): IncludeScopesMode | undefined;
7
+ /**
8
+ * Parse include-legacy-artifacts on/off tokens. undefined = invalid token.
9
+ */
10
+ export declare function tryParseOnOffFlag(value: string): boolean | undefined;
11
+ /**
12
+ * Normalize CLI / API include-scopes values to a mode (#1566).
13
+ * Default is compact (`off`) so completed lifecycle is not dumped into SPECIFICATION.md.
14
+ * Unknown string tokens throw — do not silently drop requested content.
15
+ */
16
+ export declare function normalizeIncludeScopesMode(value: boolean | IncludeScopesMode | string | undefined, defaultMode?: IncludeScopesMode): IncludeScopesMode;
1
17
  export type RenderSpecResult = readonly [boolean, string];
2
18
  export interface RenderSpecOptions {
3
- readonly includeScopes?: boolean;
19
+ /**
20
+ * Lifecycle scope aggregation (#1566).
21
+ * - `off` / false (default): no Scope outlook section
22
+ * - `current` / `active`: pending + active only
23
+ * - `all` / true / `on`: pending + active + completed (legacy dump)
24
+ */
25
+ readonly includeScopes?: boolean | IncludeScopesMode;
26
+ /** When true, emit the LegacyArtifacts narrative. Default false (#1566). */
27
+ readonly includeLegacyArtifacts?: boolean;
4
28
  }
5
29
  /** Render specification JSON to markdown (mirrors ``scripts/spec_render.render_spec``). */
6
30
  export declare function renderSpec(specPath: string, outPath: string, options?: RenderSpecOptions): RenderSpecResult;
7
31
  export declare function parseIncludeScopesFlag(argv: readonly string[]): {
8
- includeScopes: boolean;
32
+ includeScopes: IncludeScopesMode;
33
+ includeLegacyArtifacts: boolean;
9
34
  remaining: string[];
35
+ errors: string[];
10
36
  };
11
37
  /** CLI entry (mirrors ``scripts/spec_render.main``). */
12
38
  export declare function main(argv: readonly string[]): number;
@@ -1,8 +1,9 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { RENDERABLE_SPEC_STATUSES, SPEC_RENDER_BANNER, SPECIFICATION_NARRATIVE_KEY_ORDER, } from "./constants.js";
4
- import { aggregateScopeSection } from "./scope-outlook.js";
3
+ import { DEFAULT_INCLUDE_SCOPES_MODE, LEGACY_ARTIFACTS_NARRATIVE_KEY, RENDERABLE_SPEC_STATUSES, SPEC_RENDER_BANNER, SPECIFICATION_NARRATIVE_KEY_ORDER, } from "./constants.js";
4
+ import { buildScopeOutlookSection } from "./scope-outlook.js";
5
5
  import { validateSpec } from "./spec-validate.js";
6
+ import { stripTrailingWhitespace } from "./text-utils.js";
6
7
  function splitAcceptance(value) {
7
8
  if (Array.isArray(value)) {
8
9
  return value.map((item) => String(item).trim()).filter((s) => s.length > 0);
@@ -22,9 +23,63 @@ function splitAcceptance(value) {
22
23
  }
23
24
  return parts;
24
25
  }
26
+ const INCLUDE_SCOPES_OFF = new Set(["off", "false", "0", "no"]);
27
+ const INCLUDE_SCOPES_CURRENT = new Set(["current", "active"]);
28
+ const INCLUDE_SCOPES_ALL = new Set(["all", "on", "true", "1", "yes"]);
29
+ const LEGACY_ARTIFACTS_ON = new Set(["on", "true", "1", "yes"]);
30
+ const LEGACY_ARTIFACTS_OFF = new Set(["off", "false", "0", "no"]);
31
+ /**
32
+ * Parse a string include-scopes token. Returns undefined when the token is not a known mode
33
+ * (callers MUST fail closed rather than silently omitting content — #1566 Greptile P1).
34
+ */
35
+ export function tryParseIncludeScopesMode(value) {
36
+ const v = value.trim().toLowerCase();
37
+ if (INCLUDE_SCOPES_OFF.has(v))
38
+ return "off";
39
+ if (INCLUDE_SCOPES_CURRENT.has(v))
40
+ return "current";
41
+ if (INCLUDE_SCOPES_ALL.has(v))
42
+ return "all";
43
+ return undefined;
44
+ }
45
+ /**
46
+ * Parse include-legacy-artifacts on/off tokens. undefined = invalid token.
47
+ */
48
+ export function tryParseOnOffFlag(value) {
49
+ const v = value.trim().toLowerCase();
50
+ if (LEGACY_ARTIFACTS_ON.has(v))
51
+ return true;
52
+ if (LEGACY_ARTIFACTS_OFF.has(v))
53
+ return false;
54
+ return undefined;
55
+ }
56
+ /**
57
+ * Normalize CLI / API include-scopes values to a mode (#1566).
58
+ * Default is compact (`off`) so completed lifecycle is not dumped into SPECIFICATION.md.
59
+ * Unknown string tokens throw — do not silently drop requested content.
60
+ */
61
+ export function normalizeIncludeScopesMode(value, defaultMode = DEFAULT_INCLUDE_SCOPES_MODE) {
62
+ if (value === undefined)
63
+ return defaultMode;
64
+ if (value === true)
65
+ return "all";
66
+ if (value === false)
67
+ return "off";
68
+ const parsed = tryParseIncludeScopesMode(String(value));
69
+ if (parsed === undefined) {
70
+ throw new Error(`Invalid include-scopes mode '${String(value)}' (expected off|current|all)`);
71
+ }
72
+ return parsed;
73
+ }
74
+ function shouldRenderNarrativeKey(key, includeLegacyArtifacts) {
75
+ if (includeLegacyArtifacts)
76
+ return true;
77
+ return key !== LEGACY_ARTIFACTS_NARRATIVE_KEY;
78
+ }
25
79
  /** Render specification JSON to markdown (mirrors ``scripts/spec_render.render_spec``). */
26
80
  export function renderSpec(specPath, outPath, options = {}) {
27
- const includeScopes = options.includeScopes ?? true;
81
+ const includeScopesMode = normalizeIncludeScopesMode(options.includeScopes);
82
+ const includeLegacyArtifacts = options.includeLegacyArtifacts ?? false;
28
83
  const [ok, msg] = validateSpec(specPath);
29
84
  if (!ok)
30
85
  return [false, msg];
@@ -70,6 +125,8 @@ export function renderSpec(specPath, outPath, options = {}) {
70
125
  }
71
126
  const renderedKeys = new Set();
72
127
  for (const key of SPECIFICATION_NARRATIVE_KEY_ORDER) {
128
+ if (!shouldRenderNarrativeKey(key, includeLegacyArtifacts))
129
+ continue;
73
130
  const val = narratives[key];
74
131
  if (val) {
75
132
  lines.push(`## ${key}\n`);
@@ -80,6 +137,8 @@ export function renderSpec(specPath, outPath, options = {}) {
80
137
  for (const key of Object.keys(narratives).sort()) {
81
138
  if (renderedKeys.has(key) || !narratives[key])
82
139
  continue;
140
+ if (!shouldRenderNarrativeKey(key, includeLegacyArtifacts))
141
+ continue;
83
142
  lines.push(`## ${key}\n`);
84
143
  lines.push(`${String(narratives[key])}\n`);
85
144
  }
@@ -129,44 +188,82 @@ export function renderSpec(specPath, outPath, options = {}) {
129
188
  }
130
189
  }
131
190
  }
132
- if (includeScopes) {
191
+ if (includeScopesMode !== "off") {
133
192
  const vbriefDir = resolve(dirname(specPath));
134
- const scopeLines = aggregateScopeSection(vbriefDir);
193
+ const scopeLines = buildScopeOutlookSection(vbriefDir, {
194
+ includeProposed: false,
195
+ includeCompleted: includeScopesMode === "all",
196
+ });
135
197
  if (scopeLines.length > 0)
136
198
  lines.push(...scopeLines);
137
199
  }
138
- writeFileSync(outPath, lines.join("\n"), "utf8");
200
+ writeFileSync(outPath, stripTrailingWhitespace(lines.join("\n")), "utf8");
139
201
  return [true, `✓ Rendered to ${outPath}`];
140
202
  }
141
203
  export function parseIncludeScopesFlag(argv) {
142
- let includeScopes = true;
204
+ let includeScopes = DEFAULT_INCLUDE_SCOPES_MODE;
205
+ let includeLegacyArtifacts = false;
143
206
  const remaining = [];
207
+ const errors = [];
144
208
  for (const arg of argv) {
145
209
  if (arg === "--include-scopes") {
146
- includeScopes = true;
210
+ includeScopes = "all";
147
211
  continue;
148
212
  }
149
213
  if (arg.startsWith("--include-scopes=")) {
150
- const value = arg.split("=", 2)[1]?.toLowerCase() ?? "";
151
- includeScopes = value === "on" || value === "true" || value === "1" || value === "yes";
214
+ const value = arg.split("=", 2)[1] ?? "";
215
+ const parsed = tryParseIncludeScopesMode(value);
216
+ if (parsed === undefined) {
217
+ errors.push(`Invalid --include-scopes=${value} (expected off|current|all|active|on|true|1|yes|false|0|no)`);
218
+ }
219
+ else {
220
+ includeScopes = parsed;
221
+ }
222
+ continue;
223
+ }
224
+ if (arg === "--include-legacy-artifacts") {
225
+ includeLegacyArtifacts = true;
226
+ continue;
227
+ }
228
+ if (arg.startsWith("--include-legacy-artifacts=")) {
229
+ const value = arg.split("=", 2)[1] ?? "";
230
+ const parsed = tryParseOnOffFlag(value);
231
+ if (parsed === undefined) {
232
+ errors.push(`Invalid --include-legacy-artifacts=${value} (expected on|off|true|false|1|0|yes|no)`);
233
+ }
234
+ else {
235
+ includeLegacyArtifacts = parsed;
236
+ }
152
237
  continue;
153
238
  }
154
239
  remaining.push(arg);
155
240
  }
156
- return { includeScopes, remaining };
241
+ return { includeScopes, includeLegacyArtifacts, remaining, errors };
157
242
  }
158
243
  /** CLI entry (mirrors ``scripts/spec_render.main``). */
159
244
  export function main(argv) {
160
- const { includeScopes, remaining } = parseIncludeScopesFlag(argv);
245
+ const { includeScopes, includeLegacyArtifacts, remaining, errors } = parseIncludeScopesFlag(argv);
246
+ if (errors.length > 0) {
247
+ for (const err of errors)
248
+ process.stderr.write(`${err}\n`);
249
+ process.stderr.write("Usage: spec-render <spec_file> [out_file] " +
250
+ "[--include-scopes=off|current|all] [--include-legacy-artifacts=on|off]\n");
251
+ return 2;
252
+ }
161
253
  if (remaining.length === 0) {
162
- process.stderr.write("Usage: spec_render.py <spec_file> [out_file] [--include-scopes=on|off]\n");
254
+ process.stderr.write("Usage: spec-render <spec_file> [out_file] " +
255
+ "[--include-scopes=off|current|all] [--include-legacy-artifacts=on|off]\n" +
256
+ " Defaults (#1566): --include-scopes=off --include-legacy-artifacts=off\n");
163
257
  return 2;
164
258
  }
165
259
  const specPath = remaining[0] ?? "";
166
260
  const outPath = remaining.length >= 2
167
261
  ? (remaining[1] ?? "")
168
262
  : join(resolve(dirname(specPath)), "..", "SPECIFICATION.md");
169
- const [ok, message] = renderSpec(specPath, outPath, { includeScopes });
263
+ const [ok, message] = renderSpec(specPath, outPath, {
264
+ includeScopes,
265
+ includeLegacyArtifacts,
266
+ });
170
267
  process.stdout.write(`${message}\n`);
171
268
  return ok ? 0 : 1;
172
269
  }
@@ -5,4 +5,10 @@ export declare function splitWords(text: string): string[];
5
5
  /** Parse ``Phase N`` prefix for roadmap sorting (#641). */
6
6
  export declare function parsePhaseNumber(phaseName: string): number | null;
7
7
  export declare function phaseSortKey(phaseName: string): [number, number, string];
8
+ /**
9
+ * Strip trailing horizontal whitespace from each line (#1566).
10
+ * Linear scan (no regex) so long runs of tabs/spaces cannot trip ReDoS scanners.
11
+ * Preserves newline structure (including a final trailing newline).
12
+ */
13
+ export declare function stripTrailingWhitespace(text: string): string;
8
14
  //# sourceMappingURL=text-utils.d.ts.map
@@ -69,4 +69,27 @@ export function phaseSortKey(phaseName) {
69
69
  return [0, num, phaseName];
70
70
  return [1, 0, phaseName];
71
71
  }
72
+ /**
73
+ * Strip trailing horizontal whitespace from each line (#1566).
74
+ * Linear scan (no regex) so long runs of tabs/spaces cannot trip ReDoS scanners.
75
+ * Preserves newline structure (including a final trailing newline).
76
+ */
77
+ export function stripTrailingWhitespace(text) {
78
+ const lines = text.split("\n");
79
+ for (let i = 0; i < lines.length; i += 1) {
80
+ const line = lines[i] ?? "";
81
+ let end = line.length;
82
+ while (end > 0) {
83
+ const code = line.charCodeAt(end - 1);
84
+ // space, tab, non-breaking space
85
+ if (code === 0x20 || code === 0x09 || code === 0xa0)
86
+ end -= 1;
87
+ else
88
+ break;
89
+ }
90
+ if (end !== line.length)
91
+ lines[i] = line.slice(0, end);
92
+ }
93
+ return lines.join("\n");
94
+ }
72
95
  //# sourceMappingURL=text-utils.js.map
@@ -6,6 +6,9 @@ export declare function defaultWhich(name: string): string | null;
6
6
  /**
7
7
  * Return `"ghx"` if on PATH, else `"gh"`; raise if neither is present.
8
8
  * Mirrors `scripts/scm.py::resolve_binary`.
9
+ *
10
+ * On failure the message is the #2275 fail-loud diagnostic (execution-env
11
+ * boundary + remediation), not an opaque spawn error.
9
12
  */
10
13
  export declare function resolveBinary(whichFn?: WhichFn): string;
11
14
  //# sourceMappingURL=binary.d.ts.map
@@ -22,6 +22,9 @@ export function defaultWhich(name) {
22
22
  /**
23
23
  * Return `"ghx"` if on PATH, else `"gh"`; raise if neither is present.
24
24
  * Mirrors `scripts/scm.py::resolve_binary`.
25
+ *
26
+ * On failure the message is the #2275 fail-loud diagnostic (execution-env
27
+ * boundary + remediation), not an opaque spawn error.
25
28
  */
26
29
  export function resolveBinary(whichFn = defaultWhich) {
27
30
  for (const candidate of BINARY_PREFERENCE) {
@@ -29,7 +32,12 @@ export function resolveBinary(whichFn = defaultWhich) {
29
32
  return candidate;
30
33
  }
31
34
  }
32
- throw new ScmStubError("neither 'ghx' nor 'gh' found on PATH; install GitHub CLI " +
33
- "(https://cli.github.com/) or the ghx proxy (#884)");
35
+ // Keep the historical substring for existing tests / triage mappers, then
36
+ // append the #2275 execution-env remediation so agents see a named reason.
37
+ throw new ScmStubError("neither 'ghx' nor 'gh' found on PATH in this execution env; " +
38
+ "install GitHub CLI (https://cli.github.com/) or the ghx proxy (#884), " +
39
+ "or pass GH_TOKEN into a matched env and re-run; SCM-dependent gates " +
40
+ "(triage:queue, issue:ingest, pr:*, reconcile:issues, cache:fetch-all, scm:*) " +
41
+ "cannot run here. Framework-local gates do not need SCM. Refs #2275.");
34
42
  }
35
43
  //# sourceMappingURL=binary.js.map
@@ -20,6 +20,11 @@ export interface CallOptions {
20
20
  /**
21
21
  * Source-aware SCM invocation -- partial down-payment on #445 / #935 Workstream 6.
22
22
  * Mirrors `scripts/scm.py::call`.
23
+ *
24
+ * Binary absence still throws ScmStubError via resolveBinary with the #2275
25
+ * diagnostic. Full auth readiness is enforced at SCM CLI entry points
26
+ * (`scm/main`, issue-ingest, reconcile-issues) via requireScmReady — not on
27
+ * every call() — so unit tests that inject binary/seams stay hermetic.
23
28
  */
24
29
  export declare function call(source: string, verb: string, args?: readonly string[] | null, options?: CallOptions): CompletedProcess;
25
30
  //# sourceMappingURL=call.d.ts.map
package/dist/scm/call.js CHANGED
@@ -5,6 +5,11 @@ import { pyRepr } from "./py-format.js";
5
5
  /**
6
6
  * Source-aware SCM invocation -- partial down-payment on #445 / #935 Workstream 6.
7
7
  * Mirrors `scripts/scm.py::call`.
8
+ *
9
+ * Binary absence still throws ScmStubError via resolveBinary with the #2275
10
+ * diagnostic. Full auth readiness is enforced at SCM CLI entry points
11
+ * (`scm/main`, issue-ingest, reconcile-issues) via requireScmReady — not on
12
+ * every call() — so unit tests that inject binary/seams stay hermetic.
8
13
  */
9
14
  export function call(source, verb, args = null, options = {}) {
10
15
  if (!SUPPORTED_CALL_SOURCES.includes(source)) {
@@ -7,5 +7,6 @@ export * from "./errors.js";
7
7
  export * from "./gh-rest.js";
8
8
  export * from "./main.js";
9
9
  export * from "./py-format.js";
10
+ export * from "./readiness.js";
10
11
  export * from "./rest-dispatch.js";
11
12
  //# sourceMappingURL=index.d.ts.map
package/dist/scm/index.js CHANGED
@@ -7,5 +7,6 @@ export * from "./errors.js";
7
7
  export * from "./gh-rest.js";
8
8
  export * from "./main.js";
9
9
  export * from "./py-format.js";
10
+ export * from "./readiness.js";
10
11
  export * from "./rest-dispatch.js";
11
12
  //# sourceMappingURL=index.js.map