@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.
- package/dist/cache/fetch.js +3 -2
- package/dist/cache/io.d.ts +13 -2
- package/dist/cache/io.js +36 -7
- package/dist/cache/operations.js +5 -3
- package/dist/doctor/main.js +37 -0
- package/dist/doctor/openclaw-skills.d.ts +42 -8
- package/dist/doctor/openclaw-skills.js +281 -38
- package/dist/eval/health.d.ts +10 -1
- package/dist/eval/health.js +16 -16
- package/dist/hooks/dispatcher.d.ts +6 -1
- package/dist/hooks/dispatcher.js +30 -0
- package/dist/init-deposit/gitignore.js +3 -0
- package/dist/init-deposit/hygiene.d.ts +43 -1
- package/dist/init-deposit/hygiene.js +112 -7
- package/dist/init-deposit/init-deposit.d.ts +5 -0
- package/dist/init-deposit/init-deposit.js +37 -2
- package/dist/init-deposit/scaffold.js +3 -1
- package/dist/intake/github-auth-modes.d.ts +1 -1
- package/dist/intake/github-auth-modes.js +25 -3
- package/dist/intake/issue-ingest-cli.js +13 -0
- package/dist/intake/reconcile-issues-cli.js +13 -0
- package/dist/orchestration/probe-session.d.ts +5 -1
- package/dist/orchestration/probe-session.js +24 -13
- package/dist/platform/agents-md.js +1 -1
- package/dist/policy/deft-directive-disable.d.ts +86 -0
- package/dist/policy/deft-directive-disable.js +167 -0
- package/dist/policy/delivery-branch.d.ts +33 -0
- package/dist/policy/delivery-branch.js +124 -0
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +17 -1
- package/dist/policy/plan-extensions.d.ts +10 -0
- package/dist/policy/plan-extensions.js +16 -0
- package/dist/pr-closing-keywords/detect.d.ts +13 -0
- package/dist/pr-closing-keywords/detect.js +73 -8
- package/dist/pr-closing-keywords/index.d.ts +2 -2
- package/dist/pr-closing-keywords/index.js +1 -1
- package/dist/pr-closing-keywords/main.js +119 -90
- package/dist/pr-closing-keywords/types.d.ts +10 -0
- package/dist/render/constants.d.ts +16 -0
- package/dist/render/constants.js +16 -1
- package/dist/render/export-spec.d.ts +7 -1
- package/dist/render/export-spec.js +51 -5
- package/dist/render/index.d.ts +3 -2
- package/dist/render/index.js +2 -2
- package/dist/render/roadmap-render.js +164 -60
- package/dist/render/scope-outlook.d.ts +5 -0
- package/dist/render/scope-outlook.js +3 -0
- package/dist/render/spec-render.d.ts +28 -2
- package/dist/render/spec-render.js +111 -14
- package/dist/render/text-utils.d.ts +6 -0
- package/dist/render/text-utils.js +23 -0
- package/dist/scm/binary.d.ts +3 -0
- package/dist/scm/binary.js +10 -2
- package/dist/scm/call.d.ts +5 -0
- package/dist/scm/call.js +5 -0
- package/dist/scm/index.d.ts +1 -0
- package/dist/scm/index.js +1 -0
- package/dist/scm/main.d.ts +5 -0
- package/dist/scm/main.js +27 -0
- package/dist/scm/readiness-cli.d.ts +24 -0
- package/dist/scm/readiness-cli.js +85 -0
- package/dist/scm/readiness.d.ts +104 -0
- package/dist/scm/readiness.js +421 -0
- package/dist/scope/batch-promote.d.ts +26 -0
- package/dist/scope/batch-promote.js +167 -0
- package/dist/scope/brief-io.d.ts +3 -1
- package/dist/scope/brief-io.js +5 -3
- package/dist/scope/delivery-evidence.d.ts +112 -0
- package/dist/scope/delivery-evidence.js +419 -0
- package/dist/scope/index.d.ts +2 -0
- package/dist/scope/index.js +2 -0
- package/dist/scope/main.d.ts +7 -0
- package/dist/scope/main.js +155 -9
- package/dist/scope/registry-artifact-sync.js +4 -1
- package/dist/scope/transition.d.ts +11 -1
- package/dist/scope/transition.js +30 -4
- package/dist/scope/wip-cap-check.d.ts +6 -0
- package/dist/scope/wip-cap-check.js +14 -2
- package/dist/session/ritual-sentinel.js +21 -12
- package/dist/session/session-start-hook.d.ts +3 -0
- package/dist/session/session-start-hook.js +21 -0
- package/dist/session/session-start.d.ts +7 -0
- package/dist/session/session-start.js +85 -0
- package/dist/swarm/complete-cohort.d.ts +20 -1
- package/dist/swarm/complete-cohort.js +56 -9
- package/dist/swarm/finalize-cohort-cli.js +9 -0
- package/dist/swarm/finalize-cohort.d.ts +8 -0
- package/dist/swarm/finalize-cohort.js +217 -21
- package/dist/triage/welcome/index.d.ts +1 -1
- package/dist/triage/welcome/index.js +1 -1
- package/dist/triage/welcome/onboard.js +7 -1
- package/dist/triage/welcome/prior-state.d.ts +7 -0
- package/dist/triage/welcome/prior-state.js +16 -2
- package/dist/triage/welcome/writers.d.ts +10 -0
- package/dist/triage/welcome/writers.js +72 -2
- package/dist/user-config/experimental-rules.d.ts +43 -0
- package/dist/user-config/experimental-rules.js +162 -0
- package/dist/user-config/index.d.ts +1 -0
- package/dist/user-config/index.js +1 -0
- package/dist/verify-source/contained-writes.js +1 -1
- package/package.json +3 -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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
384
|
-
|
|
385
|
-
|
|
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 [
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
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
|
|
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 (
|
|
191
|
+
if (includeScopesMode !== "off") {
|
|
133
192
|
const vbriefDir = resolve(dirname(specPath));
|
|
134
|
-
const scopeLines =
|
|
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 =
|
|
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 =
|
|
210
|
+
includeScopes = "all";
|
|
147
211
|
continue;
|
|
148
212
|
}
|
|
149
213
|
if (arg.startsWith("--include-scopes=")) {
|
|
150
|
-
const value = arg.split("=", 2)[1]
|
|
151
|
-
|
|
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:
|
|
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, {
|
|
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
|
package/dist/scm/binary.d.ts
CHANGED
|
@@ -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
|
package/dist/scm/binary.js
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
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
|
package/dist/scm/call.d.ts
CHANGED
|
@@ -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)) {
|
package/dist/scm/index.d.ts
CHANGED