@opengsd/gsd-core 1.6.0-rc.2 → 1.6.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/.claude-plugin/plugin.json +2 -1
- package/agents/gsd-advisor-researcher.md +2 -0
- package/agents/gsd-ai-researcher.md +2 -0
- package/agents/gsd-assumptions-analyzer.md +2 -0
- package/agents/gsd-doc-classifier.md +2 -0
- package/agents/gsd-doc-synthesizer.md +2 -0
- package/agents/gsd-domain-researcher.md +2 -0
- package/agents/gsd-eval-auditor.md +6 -9
- package/agents/gsd-phase-researcher.md +2 -0
- package/agents/gsd-planner.md +8 -57
- package/agents/gsd-project-researcher.md +2 -0
- package/agents/gsd-research-synthesizer.md +2 -0
- package/agents/gsd-security-auditor.md +37 -18
- package/agents/gsd-ui-researcher.md +2 -0
- package/bin/install.js +370 -18
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +46 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +52 -14
- package/gsd-core/bin/lib/capability-lifecycle.cjs +30 -7
- package/gsd-core/bin/lib/capability-registry.cjs +96 -83
- package/gsd-core/bin/lib/capability-validator.cjs +22 -0
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +40 -2
- package/gsd-core/bin/lib/command-aliases.cjs +10 -1
- package/gsd-core/bin/lib/command-routing-hub.cjs +10 -3
- package/gsd-core/bin/lib/config-schema.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +67 -24
- package/gsd-core/bin/lib/coverage.cjs +464 -0
- package/gsd-core/bin/lib/decisions.cjs +27 -0
- package/gsd-core/bin/lib/eval-command-router.cjs +21 -0
- package/gsd-core/bin/lib/eval.cjs +60 -0
- package/gsd-core/bin/lib/frontmatter.cjs +132 -13
- package/gsd-core/bin/lib/graphify-command-router.cjs +53 -36
- package/gsd-core/bin/lib/init.cjs +139 -30
- package/gsd-core/bin/lib/install-profiles.cjs +6 -3
- package/gsd-core/bin/lib/intel-command-router.cjs +79 -60
- package/gsd-core/bin/lib/io.cjs +1 -0
- package/gsd-core/bin/lib/phase.cjs +16 -2
- package/gsd-core/bin/lib/plan-scan.cjs +2 -2
- package/gsd-core/bin/lib/planning-workspace.cjs +157 -13
- package/gsd-core/bin/lib/profile-output.cjs +18 -6
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +53 -16
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +21 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +15 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +47 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -0
- package/gsd-core/bin/lib/state.cjs +398 -60
- package/gsd-core/bin/lib/surface.cjs +42 -10
- package/gsd-core/bin/lib/uat-predicate.cjs +13 -6
- package/gsd-core/bin/lib/update-context.cjs +2 -2
- package/gsd-core/bin/lib/verification.cjs +67 -6
- package/gsd-core/bin/lib/verify.cjs +8 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
- package/gsd-core/references/planner-guidance.md +66 -0
- package/gsd-core/references/planning-config.md +2 -2
- package/gsd-core/references/security-asvs-levels.md +27 -0
- package/gsd-core/references/untrusted-input-boundary.md +13 -0
- package/gsd-core/templates/SECURITY.md +6 -4
- package/gsd-core/templates/summary-complex.md +4 -0
- package/gsd-core/templates/summary-minimal.md +3 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +41 -0
- package/gsd-core/workflows/autonomous.md +53 -46
- package/gsd-core/workflows/complete-milestone.md +27 -8
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/execute-plan.md +5 -0
- package/gsd-core/workflows/manager.md +17 -7
- package/gsd-core/workflows/new-project.md +82 -16
- package/gsd-core/workflows/plan-phase.md +15 -0
- package/gsd-core/workflows/profile-user.md +6 -2
- package/gsd-core/workflows/progress.md +37 -4
- package/gsd-core/workflows/quick.md +3 -1
- package/gsd-core/workflows/secure-phase.md +13 -7
- package/gsd-core/workflows/ship.md +3 -1
- package/gsd-core/workflows/spec-phase.md +3 -1
- package/gsd-core/workflows/transition.md +14 -12
- package/gsd-core/workflows/ui-review.md +2 -6
- package/gsd-core/workflows/verify-work.md +74 -1
- package/hooks/dist/gsd-read-injection-scanner.js +49 -25
- package/hooks/gsd-read-injection-scanner.js +49 -25
- package/hooks/hooks.json +1 -1
- package/package.json +4 -2
- package/scripts/check-alias-drift.cjs +5 -0
- package/scripts/gen-plugin-skills.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/prompt-injection-scan.sh +9 -0
- package/scripts/release-notes/conventional-title.cjs +88 -0
- package/scripts/release-notes/format-github-release-notes.cjs +4 -3
- package/skills/gsd-add-tests/SKILL.md +38 -0
- package/skills/gsd-ai-integration-phase/SKILL.md +37 -0
- package/skills/gsd-audit-fix/SKILL.md +33 -0
- package/skills/gsd-audit-milestone/SKILL.md +37 -0
- package/skills/gsd-audit-uat/SKILL.md +25 -0
- package/skills/gsd-autonomous/SKILL.md +51 -0
- package/skills/gsd-capture/SKILL.md +67 -0
- package/skills/gsd-cleanup/SKILL.md +24 -0
- package/skills/gsd-code-review/SKILL.md +59 -0
- package/skills/gsd-complete-milestone/SKILL.md +142 -0
- package/skills/gsd-config/SKILL.md +56 -0
- package/skills/gsd-debug/SKILL.md +53 -0
- package/skills/gsd-discuss-phase/SKILL.md +77 -0
- package/skills/gsd-docs-update/SKILL.md +49 -0
- package/skills/gsd-eval-review/SKILL.md +33 -0
- package/skills/gsd-execute-phase/SKILL.md +65 -0
- package/skills/gsd-explore/SKILL.md +28 -0
- package/skills/gsd-extract-learnings/SKILL.md +22 -0
- package/skills/gsd-fast/SKILL.md +31 -0
- package/skills/gsd-forensics/SKILL.md +56 -0
- package/skills/gsd-graphify/SKILL.md +204 -0
- package/skills/gsd-health/SKILL.md +31 -0
- package/skills/gsd-help/SKILL.md +29 -0
- package/skills/gsd-import/SKILL.md +46 -0
- package/skills/gsd-inbox/SKILL.md +39 -0
- package/skills/gsd-ingest-docs/SKILL.md +43 -0
- package/skills/gsd-manager/SKILL.md +45 -0
- package/skills/gsd-map-codebase/SKILL.md +83 -0
- package/skills/gsd-mempalace-capture/SKILL.md +71 -0
- package/skills/gsd-mempalace-recall/SKILL.md +102 -0
- package/skills/gsd-milestone-summary/SKILL.md +51 -0
- package/skills/gsd-mvp-phase/SKILL.md +45 -0
- package/skills/gsd-new-milestone/SKILL.md +45 -0
- package/skills/gsd-new-project/SKILL.md +47 -0
- package/skills/gsd-ns-context/SKILL.md +24 -0
- package/skills/gsd-ns-ideate/SKILL.md +23 -0
- package/skills/gsd-ns-manage/SKILL.md +35 -0
- package/skills/gsd-ns-project/SKILL.md +26 -0
- package/skills/gsd-ns-review/SKILL.md +28 -0
- package/skills/gsd-ns-workflow/SKILL.md +33 -0
- package/skills/gsd-pause-work/SKILL.md +43 -0
- package/skills/gsd-phase/SKILL.md +57 -0
- package/skills/gsd-plan-phase/SKILL.md +63 -0
- package/skills/gsd-plan-review-convergence/SKILL.md +60 -0
- package/skills/gsd-pr-branch/SKILL.md +26 -0
- package/skills/gsd-profile-user/SKILL.md +47 -0
- package/skills/gsd-progress/SKILL.md +49 -0
- package/skills/gsd-quick/SKILL.md +174 -0
- package/skills/gsd-resume-work/SKILL.md +31 -0
- package/skills/gsd-review/SKILL.md +42 -0
- package/skills/gsd-review-backlog/SKILL.md +63 -0
- package/skills/gsd-secure-phase/SKILL.md +36 -0
- package/skills/gsd-settings/SKILL.md +29 -0
- package/skills/gsd-ship/SKILL.md +24 -0
- package/skills/gsd-sketch/SKILL.md +60 -0
- package/skills/gsd-spec-phase/SKILL.md +63 -0
- package/skills/gsd-spike/SKILL.md +57 -0
- package/skills/gsd-stats/SKILL.md +20 -0
- package/skills/gsd-surface/SKILL.md +162 -0
- package/skills/gsd-thread/SKILL.md +24 -0
- package/skills/gsd-ui-phase/SKILL.md +35 -0
- package/skills/gsd-ui-review/SKILL.md +33 -0
- package/skills/gsd-ultraplan-phase/SKILL.md +34 -0
- package/skills/gsd-undo/SKILL.md +35 -0
- package/skills/gsd-update/SKILL.md +50 -0
- package/skills/gsd-validate-phase/SKILL.md +36 -0
- package/skills/gsd-verify-work/SKILL.md +39 -0
- package/skills/gsd-workspace/SKILL.md +53 -0
- package/skills/gsd-workstreams/SKILL.md +70 -0
|
@@ -205,20 +205,59 @@ function reconstructFrontmatter(obj) {
|
|
|
205
205
|
}
|
|
206
206
|
return lines.join('\n');
|
|
207
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Slice a frontmatter YAML body into per-top-level-key raw text segments. Each segment
|
|
210
|
+
* runs from a column-0 `key:` line through the line before the next column-0 key (or the
|
|
211
|
+
* end), capturing all nested indented content. Used by `spliceFrontmatter` for per-key
|
|
212
|
+
* identity preservation (#1572): a structurally-unchanged key keeps its original raw
|
|
213
|
+
* text, so the lossy `reconstructFrontmatter` never touches object-lists the caller did
|
|
214
|
+
* not modify (e.g. must_haves.artifacts / .prohibitions).
|
|
215
|
+
*/
|
|
216
|
+
function sliceTopLevelFrontmatterSegments(yaml) {
|
|
217
|
+
const lines = yaml.split(/\r?\n/);
|
|
218
|
+
const segments = [];
|
|
219
|
+
let current = null;
|
|
220
|
+
for (const line of lines) {
|
|
221
|
+
// A column-0 `key:` (no leading whitespace) starts a new top-level segment.
|
|
222
|
+
if (/^[A-Za-z0-9_-]+:/.test(line)) {
|
|
223
|
+
if (current)
|
|
224
|
+
segments.push({ key: current.key, raw: current.raw.join('\n') });
|
|
225
|
+
const keyName = line.match(/^([A-Za-z0-9_-]+):/)[1];
|
|
226
|
+
current = { key: keyName, raw: [line] };
|
|
227
|
+
}
|
|
228
|
+
else if (current) {
|
|
229
|
+
current.raw.push(line);
|
|
230
|
+
}
|
|
231
|
+
// Stray lines before the first top-level key (rare in frontmatter) are dropped.
|
|
232
|
+
}
|
|
233
|
+
if (current)
|
|
234
|
+
segments.push({ key: current.key, raw: current.raw.join('\n') });
|
|
235
|
+
return segments;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Regenerate one frontmatter key's serialization, fail-closed if the lossy
|
|
239
|
+
* `reconstructFrontmatter` cannot represent the value (#1572 codex review). Object-list
|
|
240
|
+
* items (e.g. must_haves.artifacts `{path, provides}` maps) serialize as the literal
|
|
241
|
+
* string "[object Object]"; rather than silently emit that and destroy the data, refuse
|
|
242
|
+
* so the caller (cmdFrontmatterSet/Merge) errors out WITHOUT writing — directing the
|
|
243
|
+
* user to edit the file directly. The reported #1572 case (mutating an UNRELATED field)
|
|
244
|
+
* is unaffected: unchanged keys preserve their original raw text and never reach here.
|
|
245
|
+
*/
|
|
246
|
+
function regenerateFrontmatterKey(key, value) {
|
|
247
|
+
const rendered = reconstructFrontmatter({ [key]: value });
|
|
248
|
+
if (/\[object Object\]/.test(rendered)) {
|
|
249
|
+
throw new Error(`frontmatter: cannot faithfully serialize key "${key}" — it contains a nested object-list ` +
|
|
250
|
+
`(e.g. must_haves.artifacts) the frontmatter writer cannot represent, and serializing it would ` +
|
|
251
|
+
`emit "[object Object]". Edit the file directly instead of using frontmatter set/merge.`);
|
|
252
|
+
}
|
|
253
|
+
return rendered;
|
|
254
|
+
}
|
|
208
255
|
function spliceFrontmatter(content, newObj) {
|
|
209
256
|
const match = content.match(/^---\r?\n[\s\S]+?\r?\n---/);
|
|
210
257
|
if (match) {
|
|
211
|
-
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
// / `{ statement, status, … }` maps). When the caller is writing back a value that is
|
|
215
|
-
// STRUCTURALLY UNCHANGED from the original parse (the canonical CRUD round-trip and the
|
|
216
|
-
// #644 prohibition schema round-trip both do this), regenerating from the lossy object would
|
|
217
|
-
// silently mangle those blocks. Detect that case by deep-equality against a re-parse of the
|
|
218
|
-
// original frontmatter and preserve the ORIGINAL raw text verbatim — a true no-op splice.
|
|
219
|
-
// This touches neither the parser (`extractFrontmatter`) nor `parseMustHavesBlock`; it only
|
|
220
|
-
// makes the existing splice faithful when nothing changed. A genuine mutation (different
|
|
221
|
-
// object) still flows through `reconstructFrontmatter` exactly as before.
|
|
258
|
+
const fmBlock = match[0];
|
|
259
|
+
// Whole-document no-op guard: a true no-op returns content verbatim (byte-exact,
|
|
260
|
+
// including any formatting the lossy serializer would normalize).
|
|
222
261
|
try {
|
|
223
262
|
if (frontmatterDeepEqual(extractFrontmatter(content), newObj)) {
|
|
224
263
|
return content;
|
|
@@ -227,10 +266,63 @@ function spliceFrontmatter(content, newObj) {
|
|
|
227
266
|
catch {
|
|
228
267
|
/* fall through to regeneration on any comparison hiccup */
|
|
229
268
|
}
|
|
230
|
-
|
|
231
|
-
|
|
269
|
+
// Per-key identity preservation (#1572). `reconstructFrontmatter` is a deliberately
|
|
270
|
+
// lossy serializer — it cannot faithfully re-emit nested object-list items (e.g.
|
|
271
|
+
// must_haves.artifacts / .prohibitions, whose items are `{ path, provides }` /
|
|
272
|
+
// `{ statement, status }` maps; `extractFrontmatter` flattens those to scalar
|
|
273
|
+
// strings, so a round-trip drops `provides:` and collapses the list to a malformed
|
|
274
|
+
// inline array). For any top-level key whose value is STRUCTURALLY UNCHANGED between
|
|
275
|
+
// the original parse and `newObj`, preserve that key's ORIGINAL raw text verbatim;
|
|
276
|
+
// regenerate only keys that actually changed. This generalizes the whole-document
|
|
277
|
+
// no-op guard above to per-key fidelity, so mutating `wave` no longer destroys an
|
|
278
|
+
// unrelated `must_haves` block. Keys absent from the original (genuinely new) are
|
|
279
|
+
// regenerated and appended; keys absent from `newObj` are preserved (never silently
|
|
280
|
+
// deleted by a set/merge).
|
|
281
|
+
const fmLines = fmBlock.split(/\r?\n/);
|
|
282
|
+
const inner = fmLines.slice(1, -1).join('\n'); // drop the opening `---` and closing `---`
|
|
283
|
+
let originalParsed;
|
|
284
|
+
try {
|
|
285
|
+
originalParsed = extractFrontmatter(fmBlock);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
originalParsed = {};
|
|
289
|
+
}
|
|
290
|
+
const segments = sliceTopLevelFrontmatterSegments(inner);
|
|
291
|
+
const emitted = [];
|
|
292
|
+
const seen = new Set();
|
|
293
|
+
for (const seg of segments) {
|
|
294
|
+
seen.add(seg.key);
|
|
295
|
+
if (Object.prototype.hasOwnProperty.call(newObj, seg.key)) {
|
|
296
|
+
// Key is in newObj: preserve original raw text if structurally unchanged,
|
|
297
|
+
// otherwise regenerate. The key SET is defined by newObj — keys that were in
|
|
298
|
+
// the original but are absent from newObj are intentionally dropped (the real
|
|
299
|
+
// cmdSet/cmdMerge flow always passes the full merged object, so this only
|
|
300
|
+
// matters for direct unit callers and matches spliceFrontmatter's contract:
|
|
301
|
+
// the result frontmatter IS newObj).
|
|
302
|
+
if (frontmatterDeepEqual(newObj[seg.key], originalParsed[seg.key])) {
|
|
303
|
+
emitted.push(seg.raw); // unchanged → preserve original raw text verbatim
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
emitted.push(regenerateFrontmatterKey(seg.key, newObj[seg.key])); // changed → regenerate (fail-closed on object-lists)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// else: key absent from newObj → drop (not emitted).
|
|
310
|
+
}
|
|
311
|
+
// Append genuinely-new keys not present in the original frontmatter.
|
|
312
|
+
for (const k of Object.keys(newObj)) {
|
|
313
|
+
if (!seen.has(k)) {
|
|
314
|
+
emitted.push(regenerateFrontmatterKey(k, newObj[k]));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const yamlStr = emitted.join('\n');
|
|
318
|
+
return `---\n${yamlStr}\n---` + content.slice(fmBlock.length);
|
|
232
319
|
}
|
|
320
|
+
// No existing frontmatter — generate from scratch, fail-closed on unrepresentable values.
|
|
233
321
|
const yamlStr = reconstructFrontmatter(newObj);
|
|
322
|
+
if (/\[object Object\]/.test(yamlStr)) {
|
|
323
|
+
throw new Error('frontmatter: cannot faithfully serialize the requested frontmatter — it contains a nested ' +
|
|
324
|
+
'object-list (e.g. must_haves.artifacts) the writer cannot represent. Edit the file directly.');
|
|
325
|
+
}
|
|
234
326
|
return `---\n${yamlStr}\n---\n\n` + content;
|
|
235
327
|
}
|
|
236
328
|
/**
|
|
@@ -434,9 +526,35 @@ function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
|
|
|
434
526
|
}
|
|
435
527
|
fm[field] = parsedValue;
|
|
436
528
|
const newContent = spliceFrontmatter(content, fm);
|
|
529
|
+
// #1660: a no-op set (newContent unchanged) with a dict-valued field means the lossy
|
|
530
|
+
// frontmatter parser made the new value's projection equal the original's — the change
|
|
531
|
+
// did not apply (bites object-list fields like must_haves). Detection lives in the pure
|
|
532
|
+
// exported helper noOpObjectListSetError so the mutation gate (property/unit set) covers
|
|
533
|
+
// it — the cmd path itself is not in that set.
|
|
534
|
+
const noOpErr = noOpObjectListSetError(content, newContent, parsedValue);
|
|
535
|
+
if (noOpErr) {
|
|
536
|
+
output({ error: noOpErr, field }, raw, undefined);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
437
539
|
(0, shell_command_projection_cjs_1.platformWriteSync)(fullPath, newContent);
|
|
438
540
|
output({ updated: true, field, value: parsedValue }, raw, 'true');
|
|
439
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* #1660: detect a frontmatter `set` that would be a silent no-op on a dict-valued field.
|
|
544
|
+
* Returns an error message when the splice produced no content change but the new value
|
|
545
|
+
* is a dict (object-list fields like must_haves, whose `{path, provides}` items flatten to
|
|
546
|
+
* scalar strings under extractFrontmatter so a replacement can deep-equal the original's
|
|
547
|
+
* projection), else null. Scalars and scalar arrays round-trip faithfully, so idempotent
|
|
548
|
+
* sets of those are intentionally NOT flagged. Pure and unit-tested directly (the cmd path
|
|
549
|
+
* is not in Stryker's property/unit set, so the detection must be testable in isolation).
|
|
550
|
+
*/
|
|
551
|
+
function noOpObjectListSetError(originalContent, newContent, parsedValue) {
|
|
552
|
+
if (newContent !== originalContent)
|
|
553
|
+
return null;
|
|
554
|
+
if (parsedValue === null || typeof parsedValue !== 'object' || Array.isArray(parsedValue))
|
|
555
|
+
return null;
|
|
556
|
+
return 'frontmatter set had no effect — the supplied value is equivalent to the existing field under the frontmatter parser, which cannot faithfully round-trip object-list fields like must_haves. Edit the file directly.';
|
|
557
|
+
}
|
|
440
558
|
function cmdFrontmatterMerge(cwd, filePath, data, raw) {
|
|
441
559
|
if (!filePath || !data) {
|
|
442
560
|
error('file and data required');
|
|
@@ -489,6 +607,7 @@ module.exports = {
|
|
|
489
607
|
parseFrontmatter: extractFrontmatter,
|
|
490
608
|
reconstructFrontmatter,
|
|
491
609
|
spliceFrontmatter,
|
|
610
|
+
noOpObjectListSetError,
|
|
492
611
|
parseMustHavesBlock,
|
|
493
612
|
FRONTMATTER_SCHEMAS,
|
|
494
613
|
cmdFrontmatterGet,
|
|
@@ -26,46 +26,63 @@
|
|
|
26
26
|
const graphify = require("./graphify.cjs");
|
|
27
27
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
28
|
const io = require("./io.cjs");
|
|
29
|
+
// Phase 2 (#1646): route through the Hub per ADR-959 §III(B) line 75.
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
|
+
const commandRoutingHub = require("./command-routing-hub.cjs");
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
33
|
+
const cjsCommandRouterAdapter = require("./cjs-command-router-adapter.cjs");
|
|
29
34
|
const { output, ERROR_REASON } = io;
|
|
35
|
+
const { makeInvalidArgs } = commandRoutingHub;
|
|
36
|
+
const { routeHubCommandFamily } = cjsCommandRouterAdapter;
|
|
30
37
|
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
31
38
|
function routeGraphifyCommand({ args, cwd, raw, error, _graphify }) {
|
|
32
|
-
const subcommand = args[1];
|
|
33
39
|
const g = _graphify ?? graphify;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
40
|
+
// Phase 2 (#1646): routes through the Command Routing Hub per ADR-959 §III(B)
|
|
41
|
+
// line 75. Validation handlers return `makeInvalidArgs(...)` Results (Q2=C,
|
|
42
|
+
// Q4=ii); the Hub → adapter translation preserves ERROR_REASON granularity
|
|
43
|
+
// via the exitReason field (Phase 1, #1644). Success handlers keep direct
|
|
44
|
+
// `output()` calls (audit's formatAuditReport quirk sets this precedent).
|
|
45
|
+
// The unknown-subcommand path is owned by the Hub's manifest check; the
|
|
46
|
+
// adapter passes SDK_UNKNOWN_COMMAND for UnknownCommand Results.
|
|
47
|
+
routeHubCommandFamily({
|
|
48
|
+
family: 'graphify',
|
|
49
|
+
args,
|
|
50
|
+
// Alphabetical order produces a stable, byte-identical `Available:` list
|
|
51
|
+
// in the unknown-subcommand message (matches the pre-conversion text).
|
|
52
|
+
subcommands: ['build', 'diff', 'query', 'status'],
|
|
53
|
+
handlers: {
|
|
54
|
+
query: () => {
|
|
55
|
+
const term = args[2];
|
|
56
|
+
if (!term) {
|
|
57
|
+
return makeInvalidArgs('term', 'Usage: gsd-tools graphify query <term>', ERROR_REASON.USAGE);
|
|
58
|
+
}
|
|
59
|
+
const budgetIdx = args.indexOf('--budget');
|
|
60
|
+
let budget = null;
|
|
61
|
+
if (budgetIdx !== -1) {
|
|
62
|
+
const rawBudget = args[budgetIdx + 1];
|
|
63
|
+
if (rawBudget === undefined || Number.isNaN(parseInt(rawBudget, 10))) {
|
|
64
|
+
return makeInvalidArgs('--budget', 'Usage: gsd-tools graphify query <term> [--budget <N>]', ERROR_REASON.USAGE);
|
|
65
|
+
}
|
|
66
|
+
budget = parseInt(rawBudget, 10);
|
|
67
|
+
}
|
|
68
|
+
output(g.graphifyQuery(cwd, term, { budget }), raw);
|
|
69
|
+
},
|
|
70
|
+
status: () => output(g.graphifyStatus(cwd), raw),
|
|
71
|
+
diff: () => output(g.graphifyDiff(cwd), raw),
|
|
72
|
+
build: () => {
|
|
73
|
+
if (args[2] === 'snapshot') {
|
|
74
|
+
output(g.writeSnapshot(cwd), raw);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
output(g.graphifyBuild(cwd), raw);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
unknownMessage: (subcommand, available) => `Unknown graphify subcommand. Available: ${available.join(', ')}`,
|
|
82
|
+
error,
|
|
83
|
+
cwd,
|
|
84
|
+
raw,
|
|
85
|
+
});
|
|
69
86
|
}
|
|
70
87
|
module.exports = {
|
|
71
88
|
routeGraphifyCommand,
|
|
@@ -43,6 +43,10 @@ const security_cjs_1 = require("./security.cjs");
|
|
|
43
43
|
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
44
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
|
|
45
45
|
const frontmatterMod = require("./frontmatter.cjs");
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
|
|
47
|
+
const verificationMod = require("./verification.cjs");
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- uat-predicate.cjs is an export= CommonJS module
|
|
49
|
+
const uatPredicateMod = require("./uat-predicate.cjs");
|
|
46
50
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- agent-install-check.cjs is an export= CommonJS module
|
|
47
51
|
const agentInstallCheck = require("./agent-install-check.cjs");
|
|
48
52
|
const { checkAgentsInstalled } = agentInstallCheck;
|
|
@@ -61,6 +65,8 @@ const { pruneOrphanedWorktrees } = worktreeSafety;
|
|
|
61
65
|
const { planningPaths, planningDir, planningRoot, findContextMdIn, } = planningWorkspace;
|
|
62
66
|
const { determinePhaseStatus } = commandsMod;
|
|
63
67
|
const { extractFrontmatter } = frontmatterMod;
|
|
68
|
+
const { readVerificationStatus } = verificationMod;
|
|
69
|
+
const { evaluateUatPassed } = uatPredicateMod;
|
|
64
70
|
// Unused but imported for structural parity
|
|
65
71
|
void stripShippedMilestones;
|
|
66
72
|
// Accept all bold/colon variants of the Requirements header (#2769)
|
|
@@ -71,6 +77,45 @@ function listPhaseSummaryFiles(phaseDir) {
|
|
|
71
77
|
function listPhasePlanFiles(phaseDir) {
|
|
72
78
|
return scanPhasePlans(phaseDir)['planFiles'];
|
|
73
79
|
}
|
|
80
|
+
function verificationNextCommand(status, phaseNumber, slashRuntime) {
|
|
81
|
+
if (status === 'gaps_found') {
|
|
82
|
+
return `${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', slashRuntime)} ${phaseNumber} --gaps`;
|
|
83
|
+
}
|
|
84
|
+
if (status === 'human_needed' || status === 'stale') {
|
|
85
|
+
return `${(0, runtime_slash_cjs_1.formatGsdSlash)('verify-work', slashRuntime)} ${phaseNumber}`;
|
|
86
|
+
}
|
|
87
|
+
if (status === 'missing' || status === 'unknown') {
|
|
88
|
+
return `${(0, runtime_slash_cjs_1.formatGsdSlash)('execute-phase', slashRuntime)} ${phaseNumber}`;
|
|
89
|
+
}
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
function projectCompletionStatus(implementationComplete, verificationPassed) {
|
|
93
|
+
if (implementationComplete && verificationPassed)
|
|
94
|
+
return 'complete';
|
|
95
|
+
if (implementationComplete)
|
|
96
|
+
return 'executed';
|
|
97
|
+
return 'incomplete';
|
|
98
|
+
}
|
|
99
|
+
function buildPhaseCompletionProjection(cwd, phaseNumber, phaseDir, planCount, summaryCount, slashRuntime) {
|
|
100
|
+
const implementationComplete = planCount > 0 && summaryCount >= planCount;
|
|
101
|
+
const phaseFullDir = phaseDir ? node_path_1.default.join(cwd, phaseDir) : '';
|
|
102
|
+
const verificationStatus = implementationComplete
|
|
103
|
+
? readVerificationStatus(phaseFullDir)
|
|
104
|
+
: { status: 'not_required', next_action: '', next_command: '' };
|
|
105
|
+
const projectedVerificationStatus = verificationStatus.status;
|
|
106
|
+
const projectedVerificationAction = verificationStatus.next_action;
|
|
107
|
+
const verificationPassed = projectedVerificationStatus === 'passed';
|
|
108
|
+
const phaseComplete = implementationComplete && verificationPassed;
|
|
109
|
+
return {
|
|
110
|
+
implementation_complete: implementationComplete,
|
|
111
|
+
verification_status: projectedVerificationStatus,
|
|
112
|
+
verification_passed: verificationPassed,
|
|
113
|
+
phase_complete: phaseComplete,
|
|
114
|
+
completion_status: projectCompletionStatus(implementationComplete, verificationPassed),
|
|
115
|
+
verification_next_action: projectedVerificationAction,
|
|
116
|
+
verification_next_command: verificationNextCommand(projectedVerificationStatus, phaseNumber, slashRuntime),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
74
119
|
function getLatestCompletedMilestone(cwd) {
|
|
75
120
|
const milestonesPath = node_path_1.default.join(planningRoot(cwd), 'MILESTONES.md');
|
|
76
121
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(milestonesPath);
|
|
@@ -636,6 +681,7 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
636
681
|
error('phase required for init verify-work');
|
|
637
682
|
}
|
|
638
683
|
const config = loadConfig(cwd);
|
|
684
|
+
const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
639
685
|
let phaseInfo = findPhaseInternal(cwd, phase);
|
|
640
686
|
if (phaseInfo?.['archived']) {
|
|
641
687
|
const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
|
|
@@ -664,15 +710,30 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
664
710
|
};
|
|
665
711
|
}
|
|
666
712
|
}
|
|
713
|
+
const phaseDir = phaseInfo?.['directory'] || null;
|
|
714
|
+
const planCount = phaseInfo?.['plans']?.length || 0;
|
|
715
|
+
const summaryCount = phaseInfo?.['summaries']?.length || 0;
|
|
716
|
+
const completion = buildPhaseCompletionProjection(cwd, phaseInfo?.['phase_number'] || phase, phaseDir, planCount, summaryCount, _slashRuntime);
|
|
717
|
+
const uatReport = phaseDir
|
|
718
|
+
? evaluateUatPassed(node_path_1.default.join(cwd, phaseDir), {
|
|
719
|
+
policy: { requireVerification: true },
|
|
720
|
+
})
|
|
721
|
+
: null;
|
|
667
722
|
const result = {
|
|
668
723
|
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
|
|
669
724
|
checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
|
|
670
725
|
commit_docs: config.commit_docs,
|
|
671
726
|
phase_found: !!phaseInfo,
|
|
672
|
-
phase_dir:
|
|
727
|
+
phase_dir: phaseDir,
|
|
673
728
|
phase_number: phaseInfo?.['phase_number'] || null,
|
|
674
729
|
phase_name: phaseInfo?.['phase_name'] || null,
|
|
675
730
|
has_verification: phaseInfo?.['has_verification'] || false,
|
|
731
|
+
phase_completion: {
|
|
732
|
+
...completion,
|
|
733
|
+
uat_passed: uatReport?.passed ?? false,
|
|
734
|
+
uat_blockers: uatReport?.blockers ?? [],
|
|
735
|
+
ready_to_transition: completion.phase_complete && (uatReport?.passed ?? false),
|
|
736
|
+
},
|
|
676
737
|
};
|
|
677
738
|
output(withProjectRoot(cwd, result), raw);
|
|
678
739
|
}
|
|
@@ -1035,18 +1096,23 @@ function cmdInitManager(cwd, raw) {
|
|
|
1035
1096
|
let hasResearch = false;
|
|
1036
1097
|
let lastActivity = null;
|
|
1037
1098
|
let isActive = false;
|
|
1099
|
+
let completion = buildPhaseCompletionProjection(cwd, phaseNum, null, planCount, summaryCount, _slashRuntime);
|
|
1038
1100
|
try {
|
|
1039
1101
|
const dirs = _phaseDirEntries.filter(isDirInMilestone);
|
|
1040
1102
|
const dirMatch = dirs.find((d) => phaseTokenMatches(d, normalized));
|
|
1041
1103
|
if (dirMatch) {
|
|
1042
1104
|
const fullDir = node_path_1.default.join(phasesDir, dirMatch);
|
|
1105
|
+
const phaseDirRel = toPosixPath(node_path_1.default.relative(cwd, fullDir));
|
|
1043
1106
|
const phaseFiles = node_fs_1.default.readdirSync(fullDir);
|
|
1044
1107
|
planCount = listPhasePlanFiles(fullDir).length;
|
|
1045
1108
|
summaryCount = listPhaseSummaryFiles(fullDir).length;
|
|
1046
1109
|
hasContext = findContextMdIn(fullDir) !== null;
|
|
1047
1110
|
hasResearch = phaseFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
1048
|
-
|
|
1111
|
+
completion = buildPhaseCompletionProjection(cwd, phaseNum, phaseDirRel, planCount, summaryCount, _slashRuntime);
|
|
1112
|
+
if (completion.phase_complete)
|
|
1049
1113
|
diskStatus = 'complete';
|
|
1114
|
+
else if (completion.implementation_complete)
|
|
1115
|
+
diskStatus = 'executed';
|
|
1050
1116
|
else if (summaryCount > 0)
|
|
1051
1117
|
diskStatus = 'partial';
|
|
1052
1118
|
else if (planCount > 0)
|
|
@@ -1079,7 +1145,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
1079
1145
|
/* intentionally empty */
|
|
1080
1146
|
}
|
|
1081
1147
|
const roadmapComplete = _checkboxStates.get(phaseNum) || false;
|
|
1082
|
-
if (roadmapComplete && diskStatus !== 'complete') {
|
|
1148
|
+
if (roadmapComplete && completion.phase_complete && diskStatus !== 'complete') {
|
|
1083
1149
|
diskStatus = 'complete';
|
|
1084
1150
|
}
|
|
1085
1151
|
phases.push({
|
|
@@ -1093,6 +1159,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
1093
1159
|
plan_count: planCount,
|
|
1094
1160
|
summary_count: summaryCount,
|
|
1095
1161
|
roadmap_complete: roadmapComplete,
|
|
1162
|
+
...completion,
|
|
1096
1163
|
last_activity: lastActivity,
|
|
1097
1164
|
is_active: isActive,
|
|
1098
1165
|
});
|
|
@@ -1107,22 +1174,42 @@ function cmdInitManager(cwd, raw) {
|
|
|
1107
1174
|
phase['display_name'] = name;
|
|
1108
1175
|
}
|
|
1109
1176
|
}
|
|
1110
|
-
|
|
1177
|
+
function normalizePhaseNumber(value) {
|
|
1178
|
+
return value
|
|
1179
|
+
.split('.')
|
|
1180
|
+
.map((part) => {
|
|
1181
|
+
const match = /^(\d+)([A-Z]?)$/i.exec(part);
|
|
1182
|
+
if (!match)
|
|
1183
|
+
return part;
|
|
1184
|
+
return `${Number(match[1])}${match[2].toUpperCase()}`;
|
|
1185
|
+
})
|
|
1186
|
+
.join('.');
|
|
1187
|
+
}
|
|
1188
|
+
const completedNums = new Set(phases
|
|
1189
|
+
.filter((p) => p['phase_complete'] === true)
|
|
1190
|
+
.map((p) => normalizePhaseNumber(p['number'])));
|
|
1191
|
+
const phaseMap = new Map(phases.map((p) => [normalizePhaseNumber(p['number']), p]));
|
|
1111
1192
|
const _allCompletedPattern = /-\s*\[x\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi;
|
|
1112
1193
|
let _allMatch;
|
|
1113
1194
|
while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
|
|
1114
|
-
|
|
1195
|
+
const phaseNum = normalizePhaseNumber(_allMatch[1]);
|
|
1196
|
+
const phase = phaseMap.get(phaseNum);
|
|
1197
|
+
if (!phase || phase['phase_complete'] === true) {
|
|
1198
|
+
completedNums.add(phaseNum);
|
|
1199
|
+
}
|
|
1115
1200
|
}
|
|
1116
|
-
const phaseMap = new Map(phases.map((p) => [p['number'], p]));
|
|
1117
1201
|
function reaches(from, to, visited = new Set()) {
|
|
1118
|
-
|
|
1202
|
+
const normalizedFrom = normalizePhaseNumber(from);
|
|
1203
|
+
const normalizedTo = normalizePhaseNumber(to);
|
|
1204
|
+
if (visited.has(normalizedFrom))
|
|
1119
1205
|
return false;
|
|
1120
|
-
visited.add(
|
|
1121
|
-
const p = phaseMap.get(
|
|
1206
|
+
visited.add(normalizedFrom);
|
|
1207
|
+
const p = phaseMap.get(normalizedFrom);
|
|
1122
1208
|
if (!p || !p['dep_phases'] || p['dep_phases'].length === 0)
|
|
1123
1209
|
return false;
|
|
1124
|
-
if (p['dep_phases'].
|
|
1210
|
+
if (p['dep_phases'].some((dep) => normalizePhaseNumber(dep) === normalizedTo)) {
|
|
1125
1211
|
return true;
|
|
1212
|
+
}
|
|
1126
1213
|
return p['dep_phases'].some((dep) => reaches(dep, to, visited));
|
|
1127
1214
|
}
|
|
1128
1215
|
function hasDepRelationship(numA, numB) {
|
|
@@ -1134,8 +1221,8 @@ function cmdInitManager(cwd, raw) {
|
|
|
1134
1221
|
phase['deps_satisfied'] = true;
|
|
1135
1222
|
}
|
|
1136
1223
|
else {
|
|
1137
|
-
const depNums = phase['depends_on'].match(/\d+(?:\.\d+)*/
|
|
1138
|
-
phase['deps_satisfied'] = depNums.every((n) => completedNums.has(n));
|
|
1224
|
+
const depNums = phase['depends_on'].match(/\d+[A-Z]?(?:\.\d+)*/gi) || [];
|
|
1225
|
+
phase['deps_satisfied'] = depNums.every((n) => completedNums.has(normalizePhaseNumber(n)));
|
|
1139
1226
|
phase['dep_phases'] = depNums;
|
|
1140
1227
|
}
|
|
1141
1228
|
}
|
|
@@ -1167,7 +1254,16 @@ function cmdInitManager(cwd, raw) {
|
|
|
1167
1254
|
continue;
|
|
1168
1255
|
if (/^999(?:\.|$)/.test(phase['number']))
|
|
1169
1256
|
continue;
|
|
1170
|
-
if (phase['disk_status'] === '
|
|
1257
|
+
if (phase['disk_status'] === 'executed') {
|
|
1258
|
+
recommendedActions.push({
|
|
1259
|
+
phase: phase['number'],
|
|
1260
|
+
phase_name: phase['name'],
|
|
1261
|
+
action: 'verify',
|
|
1262
|
+
reason: `Implementation complete; verification ${phase['verification_status']}`,
|
|
1263
|
+
command: phase['verification_next_command'],
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
else if (phase['disk_status'] === 'planned' && phase['deps_satisfied']) {
|
|
1171
1267
|
recommendedActions.push({
|
|
1172
1268
|
phase: phase['number'],
|
|
1173
1269
|
phase_name: phase['name'],
|
|
@@ -1211,7 +1307,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
1211
1307
|
return true;
|
|
1212
1308
|
});
|
|
1213
1309
|
const nonBacklogPhases = phases.filter((p) => !/^999(?:\.|$)/.test(p['number']));
|
|
1214
|
-
const completedCount = nonBacklogPhases.filter((p) => p['
|
|
1310
|
+
const completedCount = nonBacklogPhases.filter((p) => p['phase_complete'] === true).length;
|
|
1215
1311
|
const sanitizeFlags = (rawVal) => {
|
|
1216
1312
|
const val = typeof rawVal === 'string' ? rawVal : '';
|
|
1217
1313
|
if (!val)
|
|
@@ -1238,7 +1334,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
1238
1334
|
phases,
|
|
1239
1335
|
phase_count: phases.length,
|
|
1240
1336
|
completed_count: completedCount,
|
|
1241
|
-
in_progress_count: phases.filter((p) => ['partial', 'planned', 'discussed', 'researched'].includes(p['disk_status'])).length,
|
|
1337
|
+
in_progress_count: phases.filter((p) => ['executed', 'partial', 'planned', 'discussed', 'researched'].includes(p['disk_status'])).length,
|
|
1242
1338
|
recommended_actions: filteredActions,
|
|
1243
1339
|
waiting_signal: waitingSignal,
|
|
1244
1340
|
all_complete: completedCount === nonBacklogPhases.length && nonBacklogPhases.length > 0,
|
|
@@ -1258,6 +1354,7 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1258
1354
|
}
|
|
1259
1355
|
const config = loadConfig(cwd);
|
|
1260
1356
|
const milestone = getMilestoneInfo(cwd);
|
|
1357
|
+
const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
1261
1358
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
1262
1359
|
const phases = [];
|
|
1263
1360
|
let currentPhase = null;
|
|
@@ -1307,24 +1404,29 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1307
1404
|
const plans = listPhasePlanFiles(phasePath);
|
|
1308
1405
|
const summaries = listPhaseSummaryFiles(phasePath);
|
|
1309
1406
|
const hasResearch = phaseFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
1310
|
-
const
|
|
1407
|
+
const phaseDirRel = toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'phases', dir)));
|
|
1408
|
+
const completion = buildPhaseCompletionProjection(cwd, phaseNumber, phaseDirRel, plans.length, summaries.length, _slashRuntime);
|
|
1409
|
+
const status = completion.phase_complete
|
|
1311
1410
|
? 'complete'
|
|
1312
|
-
:
|
|
1313
|
-
? '
|
|
1314
|
-
:
|
|
1315
|
-
? '
|
|
1316
|
-
:
|
|
1411
|
+
: completion.implementation_complete
|
|
1412
|
+
? 'executed'
|
|
1413
|
+
: plans.length > 0
|
|
1414
|
+
? 'in_progress'
|
|
1415
|
+
: hasResearch
|
|
1416
|
+
? 'researched'
|
|
1417
|
+
: 'pending';
|
|
1317
1418
|
const phaseInfo = {
|
|
1318
1419
|
number: phaseNumber,
|
|
1319
1420
|
name: phaseName,
|
|
1320
|
-
directory:
|
|
1421
|
+
directory: phaseDirRel,
|
|
1321
1422
|
status,
|
|
1322
1423
|
plan_count: plans.length,
|
|
1323
1424
|
summary_count: summaries.length,
|
|
1324
1425
|
has_research: hasResearch,
|
|
1426
|
+
...completion,
|
|
1325
1427
|
};
|
|
1326
1428
|
phases.push(phaseInfo);
|
|
1327
|
-
if (!currentPhase && (status === 'in_progress' || status === 'researched')) {
|
|
1429
|
+
if (!currentPhase && (status === 'executed' || status === 'in_progress' || status === 'researched')) {
|
|
1328
1430
|
currentPhase = phaseInfo;
|
|
1329
1431
|
}
|
|
1330
1432
|
if (!nextPhase && status === 'pending') {
|
|
@@ -1340,7 +1442,8 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1340
1442
|
if (!seenPhaseNums.has(stripped)) {
|
|
1341
1443
|
const checkboxComplete = roadmapCheckboxStates.get(num) === true ||
|
|
1342
1444
|
roadmapCheckboxStates.get(stripped) === true;
|
|
1343
|
-
const
|
|
1445
|
+
const completion = buildPhaseCompletionProjection(cwd, num, null, 0, 0, _slashRuntime);
|
|
1446
|
+
const status = 'not_started';
|
|
1344
1447
|
const phaseInfo = {
|
|
1345
1448
|
number: num,
|
|
1346
1449
|
name: name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''),
|
|
@@ -1349,9 +1452,11 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1349
1452
|
plan_count: 0,
|
|
1350
1453
|
summary_count: 0,
|
|
1351
1454
|
has_research: false,
|
|
1455
|
+
roadmap_complete: checkboxComplete,
|
|
1456
|
+
...completion,
|
|
1352
1457
|
};
|
|
1353
1458
|
phases.push(phaseInfo);
|
|
1354
|
-
if (!nextPhase && !currentPhase &&
|
|
1459
|
+
if (!nextPhase && !currentPhase && !checkboxComplete) {
|
|
1355
1460
|
nextPhase = phaseInfo;
|
|
1356
1461
|
}
|
|
1357
1462
|
}
|
|
@@ -1373,7 +1478,7 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1373
1478
|
phases,
|
|
1374
1479
|
phase_count: phases.length,
|
|
1375
1480
|
completed_count: phases.filter((p) => p['status'] === 'complete').length,
|
|
1376
|
-
in_progress_count: phases.filter((p) =>
|
|
1481
|
+
in_progress_count: phases.filter((p) => ['executed', 'in_progress'].includes(p['status'])).length,
|
|
1377
1482
|
current_phase: currentPhase,
|
|
1378
1483
|
next_phase: nextPhase,
|
|
1379
1484
|
paused_at: pausedAt,
|
|
@@ -1724,10 +1829,14 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
|
|
|
1724
1829
|
}, raw);
|
|
1725
1830
|
return;
|
|
1726
1831
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
process.exit(
|
|
1832
|
+
// #1400: emit the raw block via the synchronous-flush output() helper (the same
|
|
1833
|
+
// one the --json branch uses) rather than process.stdout.write + process.exit(0).
|
|
1834
|
+
// When stdout is a pipe/file (how workflows consume this via command
|
|
1835
|
+
// substitution) the async stdout buffer is torn down by process.exit() before
|
|
1836
|
+
// it drains — on Windows this reliably truncates the write to 0 bytes, so every
|
|
1837
|
+
// ${AGENT_SKILLS_*} substitution expands empty. output() writes every byte with
|
|
1838
|
+
// writeAllSync and returns, letting the event loop drain naturally.
|
|
1839
|
+
output(block || '', true, block || '');
|
|
1731
1840
|
}
|
|
1732
1841
|
function buildSkillManifest(cwd, skillsDir = null) {
|
|
1733
1842
|
const canonicalRoots = skillsDir
|
|
@@ -523,9 +523,12 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
|
|
|
523
523
|
*
|
|
524
524
|
* @param srcAgentsDir source agents directory (e.g. agents/)
|
|
525
525
|
* @param resolvedProfile profile filter from resolveProfile()
|
|
526
|
-
* @param converter (content: string) → string
|
|
526
|
+
* @param converter (content: string, isGlobal?: boolean) → string per-file
|
|
527
|
+
* converter; scope-aware converters (copilot/antigravity)
|
|
528
|
+
* read isGlobal, single-arg converters ignore it (#1173)
|
|
529
|
+
* @param isGlobal install scope passed through to the converter
|
|
527
530
|
*/
|
|
528
|
-
function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, converter) {
|
|
531
|
+
function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, converter, isGlobal = false) {
|
|
529
532
|
if (!node_fs_1.default.existsSync(srcAgentsDir))
|
|
530
533
|
return srcAgentsDir;
|
|
531
534
|
const stageDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'gsd-profile-runtime-agents-'));
|
|
@@ -544,7 +547,7 @@ function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, conve
|
|
|
544
547
|
}
|
|
545
548
|
}
|
|
546
549
|
const content = node_fs_1.default.readFileSync(node_path_1.default.join(srcAgentsDir, entry.name), 'utf8');
|
|
547
|
-
const converted = converter(content);
|
|
550
|
+
const converted = converter(content, isGlobal);
|
|
548
551
|
node_fs_1.default.writeFileSync(node_path_1.default.join(stageDir, entry.name), converted, 'utf8');
|
|
549
552
|
}
|
|
550
553
|
}
|