@opengsd/gsd-core 1.5.0-rc.2 → 1.5.0-rc.3
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 +1 -1
- package/agents/gsd-mempalace-curator.md +47 -0
- package/bin/install.js +25 -7
- package/commands/gsd/mempalace-capture.md +71 -0
- package/commands/gsd/mempalace-recall.md +102 -0
- package/commands/gsd/ns-context.md +4 -2
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +110 -92
- package/gsd-core/bin/lib/capability-activation.cjs +86 -0
- package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
- package/gsd-core/bin/lib/capability-state.cjs +31 -18
- package/gsd-core/bin/lib/check-command-router.cjs +328 -1
- package/gsd-core/bin/lib/clusters.cjs +2 -0
- package/gsd-core/bin/lib/command-roster.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +33 -10
- package/gsd-core/bin/lib/config-loader.cjs +7 -8
- package/gsd-core/bin/lib/config-schema.cjs +32 -3
- package/gsd-core/bin/lib/config.cjs +1 -2
- package/gsd-core/bin/lib/core.cjs +5 -2
- package/gsd-core/bin/lib/init.cjs +8 -5
- package/gsd-core/bin/lib/install-profiles.cjs +2 -1
- package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
- package/gsd-core/bin/lib/model-resolver.cjs +47 -5
- package/gsd-core/bin/lib/phase.cjs +71 -11
- package/gsd-core/bin/lib/profile-output.cjs +45 -4
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
- package/gsd-core/bin/lib/roadmap.cjs +97 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1700 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +20 -28
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
- package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
- package/gsd-core/bin/lib/state-command-router.cjs +15 -3
- package/gsd-core/bin/lib/state-document.cjs +46 -1
- package/gsd-core/bin/lib/state.cjs +221 -35
- package/gsd-core/bin/lib/verify.cjs +19 -4
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
- package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
- package/gsd-core/bin/shared/model-catalog.json +1 -0
- package/gsd-core/references/loop-hook-dispatch.md +61 -0
- package/gsd-core/templates/config.json +1 -1
- package/gsd-core/workflows/audit-milestone.md +5 -3
- package/gsd-core/workflows/autonomous.md +10 -5
- package/gsd-core/workflows/code-review-fix.md +9 -7
- package/gsd-core/workflows/code-review.md +8 -6
- package/gsd-core/workflows/discuss-phase.md +14 -0
- package/gsd-core/workflows/execute-phase.md +82 -143
- package/gsd-core/workflows/help/modes/full.md +3 -1
- package/gsd-core/workflows/new-project.md +3 -3
- package/gsd-core/workflows/plan-phase.md +89 -221
- package/gsd-core/workflows/profile-user.md +3 -1
- package/gsd-core/workflows/progress.md +58 -1
- package/gsd-core/workflows/quick.md +9 -6
- package/gsd-core/workflows/review.md +19 -2
- package/gsd-core/workflows/secure-phase.md +4 -2
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/ship.md +57 -0
- package/gsd-core/workflows/validate-phase.md +4 -2
- package/gsd-core/workflows/verify-work.md +27 -11
- package/package.json +3 -2
- package/scripts/affected-tests-lib.cjs +16 -4
- package/scripts/gen-capability-registry.cjs +359 -48
- package/scripts/gen-inventory-manifest.cjs +1 -4
- package/scripts/gen-loop-host-contract.cjs +55 -0
- package/scripts/issue-version-gate.cjs +140 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +326 -0
- package/scripts/lint-allow-test-rule-refs.cjs +162 -0
- package/scripts/lint-test-file-count.allowlist.json +8 -0
- package/scripts/release-tarball-smoke.cjs +7 -1
- package/scripts/run-tests.cjs +74 -14
|
@@ -245,34 +245,76 @@ function updateCurrentPositionFields(content, fields) {
|
|
|
245
245
|
let posBody = posMatch[2];
|
|
246
246
|
const statusDefaults = state_document_cjs_1.KNOWN_TEMPLATE_DEFAULTS['Status'];
|
|
247
247
|
const lastActivityDefaults = state_document_cjs_1.KNOWN_TEMPLATE_DEFAULTS['Last Activity'];
|
|
248
|
-
if (fields.status
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
248
|
+
if (fields.status) {
|
|
249
|
+
if (/^Status:/m.test(posBody)) {
|
|
250
|
+
// Inline format: Status: value — only replace when the existing value is a
|
|
251
|
+
// known template default (Knuth invariant: preserve executor-authored values).
|
|
252
|
+
const existingStatusMatch = posBody.match(/^Status:\s*(.+)$/m);
|
|
253
|
+
const existingStatus = existingStatusMatch ? existingStatusMatch[1].trim() : null;
|
|
254
|
+
const isInList = existingStatus && statusDefaults.some(d => d.toLowerCase() === existingStatus.toLowerCase());
|
|
255
|
+
const matchesPattern = existingStatus && state_document_cjs_1.KNOWN_STATUS_PATTERNS.some(p => p.test(existingStatus));
|
|
256
|
+
const isDefault = !existingStatus || isInList || matchesPattern;
|
|
257
|
+
if (isDefault) {
|
|
258
|
+
posBody = posBody.replace(/^Status:.*$/m, `Status: ${fields.status}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
// Table format: | Status | value | — apply the same preserve-authored guard
|
|
263
|
+
// as the inline branch: only overwrite a known template default.
|
|
264
|
+
// (Finding 2 code-review: the table branch was unconditional before this fix.)
|
|
265
|
+
const existingStatus = (0, state_document_cjs_1.stateExtractField)(posBody, 'Status');
|
|
266
|
+
const isInList = existingStatus && statusDefaults.some(d => d.toLowerCase() === existingStatus.toLowerCase());
|
|
267
|
+
const matchesPattern = existingStatus && state_document_cjs_1.KNOWN_STATUS_PATTERNS.some(p => p.test(existingStatus));
|
|
268
|
+
const isDefault = !existingStatus || isInList || matchesPattern;
|
|
269
|
+
if (isDefault) {
|
|
270
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Status', fields.status);
|
|
271
|
+
if (replaced !== null)
|
|
272
|
+
posBody = replaced;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (fields.lastActivity) {
|
|
277
|
+
if (/^Last activity:/im.test(posBody)) {
|
|
278
|
+
// Inline format — only replace when the existing value is a known template
|
|
279
|
+
// default (a bare ISO date). Executor-authored narrative prose is preserved.
|
|
280
|
+
const existingActivityMatch = posBody.match(/^Last activity:\s*(.+)$/im);
|
|
281
|
+
const existingActivity = existingActivityMatch ? existingActivityMatch[1].trim() : null;
|
|
282
|
+
// A bare ISO date (YYYY-MM-DD with nothing after) is handler-generated.
|
|
283
|
+
// A date with a narrative suffix (e.g. "2026-02-15 -- blocked by infra...")
|
|
284
|
+
// was authored by the executor and must be preserved.
|
|
285
|
+
const isDateShape = existingActivity && /^\d{4}-\d{2}-\d{2}$/.test(existingActivity);
|
|
286
|
+
const inList = existingActivity && lastActivityDefaults.some(d => d.toLowerCase() === existingActivity.toLowerCase());
|
|
287
|
+
const isDefault = !existingActivity || isDateShape || inList;
|
|
288
|
+
if (isDefault) {
|
|
289
|
+
posBody = posBody.replace(/^Last activity:.*$/im, `Last activity: ${fields.lastActivity}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// Table format — apply the same preserve-authored guard as the inline branch:
|
|
294
|
+
// only overwrite a bare ISO date or a known default; preserve narrative prose.
|
|
295
|
+
// (Finding 2 code-review: the table branch was unconditional before this fix.)
|
|
296
|
+
const existingActivity = (0, state_document_cjs_1.stateExtractField)(posBody, 'Last Activity')
|
|
297
|
+
?? (0, state_document_cjs_1.stateExtractField)(posBody, 'Last activity');
|
|
298
|
+
const isDateShape = existingActivity && /^\d{4}-\d{2}-\d{2}$/.test(existingActivity);
|
|
299
|
+
const inList = existingActivity && lastActivityDefaults.some(d => d.toLowerCase() === existingActivity.toLowerCase());
|
|
300
|
+
const isDefault = !existingActivity || isDateShape || inList;
|
|
301
|
+
if (isDefault) {
|
|
302
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last Activity', fields.lastActivity)
|
|
303
|
+
?? (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last activity', fields.lastActivity);
|
|
304
|
+
if (replaced !== null)
|
|
305
|
+
posBody = replaced;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (fields.plan) {
|
|
310
|
+
if (/^Plan:/m.test(posBody)) {
|
|
311
|
+
posBody = posBody.replace(/^Plan:.*$/m, `Plan: ${fields.plan}`);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Plan', fields.plan);
|
|
315
|
+
if (replaced !== null)
|
|
316
|
+
posBody = replaced;
|
|
317
|
+
}
|
|
276
318
|
}
|
|
277
319
|
return content.replace(posPattern, () => `${posMatch[1]}${posBody}`);
|
|
278
320
|
}
|
|
@@ -560,6 +602,101 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
560
602
|
result['created'] = true;
|
|
561
603
|
output(result, raw, 'true');
|
|
562
604
|
}
|
|
605
|
+
function cmdStateAddRoadmapEvolution(cwd, options, raw) {
|
|
606
|
+
const statePath = planningPaths(cwd).state;
|
|
607
|
+
if (!node_fs_1.default.existsSync(statePath)) {
|
|
608
|
+
output({ error: 'STATE.md not found' }, raw, undefined);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const { phase, action, after, note, note_file, urgent } = options;
|
|
612
|
+
let noteText = undefined;
|
|
613
|
+
try {
|
|
614
|
+
noteText = readTextArgOrFile(cwd, note, note_file, 'note');
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
output({ added: false, reason: err.message }, raw, 'false');
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// Reject missing / empty / whitespace-only notes — an evolution entry with no
|
|
621
|
+
// narrative is meaningless and would corrupt the section with a dangling bullet.
|
|
622
|
+
if (!noteText || !noteText.trim()) {
|
|
623
|
+
output({ error: 'note required' }, raw, undefined);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
// Flatten line breaks so the entry is always a single Markdown bullet. The
|
|
627
|
+
// dedupe + rendering contract is line-oriented; a multiline --note-file would
|
|
628
|
+
// otherwise spill continuation lines outside the bullet and defeat dedupe.
|
|
629
|
+
// Internal spacing (e.g. dollar columns) is preserved.
|
|
630
|
+
const flatNote = noteText.replace(/\s*[\r\n]+\s*/g, ' ').trim();
|
|
631
|
+
const actionText = (action && action.trim()) || 'changed';
|
|
632
|
+
const afterText = after && after.trim() ? ` after Phase ${after.trim()}` : '';
|
|
633
|
+
const urgentText = urgent ? ' (URGENT)' : '';
|
|
634
|
+
const entry = `- Phase ${phase || '?'} ${actionText}${afterText}: ${flatNote}${urgentText}`;
|
|
635
|
+
let duplicate = false;
|
|
636
|
+
let created = false;
|
|
637
|
+
let subsectionCreated = false;
|
|
638
|
+
// The Roadmap Evolution subsection lives under `## Accumulated Context`. Scope
|
|
639
|
+
// every lookup to that section's body so a `### Roadmap Evolution` heading in an
|
|
640
|
+
// unrelated h2 section (or a fenced example) can never be matched or mutated.
|
|
641
|
+
// The accBody lookahead stops only at the next h2 (`\n##[^#]`), so nested h3
|
|
642
|
+
// subsections stay inside the captured Accumulated Context body.
|
|
643
|
+
// Section boundaries mirror the sibling handlers (add-decision/add-blocker):
|
|
644
|
+
// a trailing CR on a CRLF STATE.md is absorbed by the lazy body and trimmed,
|
|
645
|
+
// so following sections are preserved without data loss (see the CRLF test).
|
|
646
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
647
|
+
const accPattern = /(##\s*Accumulated Context\s*\n)([\s\S]*?)(?=\n##[^#]|$)/i;
|
|
648
|
+
const accMatch = content.match(accPattern);
|
|
649
|
+
if (accMatch) {
|
|
650
|
+
const accHeader = accMatch[1];
|
|
651
|
+
const accBody = accMatch[2];
|
|
652
|
+
// Find `### Roadmap Evolution` WITHIN the Accumulated Context body only.
|
|
653
|
+
// Bounded by the next h3/h2 or the end of the section body.
|
|
654
|
+
const subPattern = /(###\s*Roadmap Evolution\s*\n)([\s\S]*?)(?=\n###?|$)/i;
|
|
655
|
+
const subMatch = accBody.match(subPattern);
|
|
656
|
+
if (subMatch) {
|
|
657
|
+
let subBody = subMatch[2];
|
|
658
|
+
// Dedupe: exact (trimmed) line already present is a no-op replay.
|
|
659
|
+
if (subBody.split('\n').some((line) => line.trim() === entry.trim())) {
|
|
660
|
+
duplicate = true;
|
|
661
|
+
return content;
|
|
662
|
+
}
|
|
663
|
+
subBody = subBody.replace(/None yet\.?\s*\n?/gi, '');
|
|
664
|
+
subBody = subBody.trimEnd() + '\n' + entry + '\n';
|
|
665
|
+
const newAccBody = accBody.replace(subPattern, (_m, header) => `${header}${subBody}`);
|
|
666
|
+
return content.replace(accPattern, () => `${accHeader}${newAccBody}`);
|
|
667
|
+
}
|
|
668
|
+
// Subsection missing — append it at the end of the Accumulated Context body.
|
|
669
|
+
subsectionCreated = true;
|
|
670
|
+
const trimmedAcc = accBody.trimEnd();
|
|
671
|
+
const block = `${trimmedAcc ? `${trimmedAcc}\n\n` : ''}### Roadmap Evolution\n\n${entry}\n`;
|
|
672
|
+
return content.replace(accPattern, () => `${accHeader}${block}`);
|
|
673
|
+
}
|
|
674
|
+
// No `## Accumulated Context` — DWIM: create both at end of file.
|
|
675
|
+
// Mirrors the add-decision / add-blocker auto-create behavior.
|
|
676
|
+
created = true;
|
|
677
|
+
subsectionCreated = true;
|
|
678
|
+
const scaffold = [
|
|
679
|
+
'',
|
|
680
|
+
'## Accumulated Context',
|
|
681
|
+
'',
|
|
682
|
+
'### Roadmap Evolution',
|
|
683
|
+
'',
|
|
684
|
+
entry,
|
|
685
|
+
'',
|
|
686
|
+
].join('\n');
|
|
687
|
+
return content.trimEnd() + '\n' + scaffold;
|
|
688
|
+
}, cwd);
|
|
689
|
+
if (duplicate) {
|
|
690
|
+
output({ added: false, reason: 'duplicate', entry }, raw, 'false');
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const result = { added: true, entry };
|
|
694
|
+
if (created)
|
|
695
|
+
result['created'] = true;
|
|
696
|
+
if (subsectionCreated)
|
|
697
|
+
result['subsection_created'] = true;
|
|
698
|
+
output(result, raw, 'true');
|
|
699
|
+
}
|
|
563
700
|
function cmdStateResolveBlocker(cwd, text, raw) {
|
|
564
701
|
const statePath = planningPaths(cwd).state;
|
|
565
702
|
if (!node_fs_1.default.existsSync(statePath)) {
|
|
@@ -679,7 +816,9 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
679
816
|
// newly-written Stopped at / Resume file end up in the second (invisible) block.
|
|
680
817
|
// Fix: when a `## Session` heading already exists, normalize THAT block in place
|
|
681
818
|
// (insert / replace canonical bold-label lines within the existing section).
|
|
682
|
-
//
|
|
819
|
+
// A `## Session Continuity` heading (bootstrap shape) is handled additively —
|
|
820
|
+
// missing canonical fields are inserted while the heading and any prose are
|
|
821
|
+
// preserved (#1101). Only append a brand-new section when NEITHER heading exists.
|
|
683
822
|
const callerSuppliedValues = !!(options.stopped_at || (options.resume_file !== undefined && options.resume_file !== null));
|
|
684
823
|
const needsStoppedAt = options.stopped_at && !updated.includes('Stopped At');
|
|
685
824
|
const needsResumeFile = options.resume_file !== undefined && options.resume_file !== null && !updated.includes('Resume File');
|
|
@@ -689,9 +828,14 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
689
828
|
? options.resume_file
|
|
690
829
|
: 'None';
|
|
691
830
|
const stoppedAtValue = options.stopped_at || 'None';
|
|
692
|
-
// Determine whether a
|
|
693
|
-
|
|
694
|
-
|
|
831
|
+
// Determine whether a session heading already exists in the body. The
|
|
832
|
+
// canonical normalized form is `## Session`; the bootstrap templates
|
|
833
|
+
// (workstream.cts, gsd2-import.cts, templates/state.md) instead emit
|
|
834
|
+
// `## Session Continuity`. Treat each separately so we never append a
|
|
835
|
+
// duplicate section alongside an existing one.
|
|
836
|
+
const existingCanonicalSession = /^## Session[ \t]*$/im.test(content);
|
|
837
|
+
const existingSessionContinuity = /^## Session Continuity[ \t]*$/im.test(content);
|
|
838
|
+
if (existingCanonicalSession) {
|
|
695
839
|
// Normalize in place: replace the ENTIRE BODY of the existing ## Session
|
|
696
840
|
// section (heading + all content up to the next ## heading or EOF) with
|
|
697
841
|
// canonical bold-label lines. The negative-lookahead per-line pattern
|
|
@@ -708,8 +852,31 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
708
852
|
'',
|
|
709
853
|
].join('\n'));
|
|
710
854
|
}
|
|
855
|
+
else if (existingSessionContinuity) {
|
|
856
|
+
// #1101: a `## Session Continuity` section already exists (bootstrap
|
|
857
|
+
// shape). Previously this fell through to the append branch and created
|
|
858
|
+
// a SECOND `## Session` block — a duplicate. Instead, insert only the
|
|
859
|
+
// canonical fields that are still missing, right after the heading,
|
|
860
|
+
// preserving the `## Session Continuity` heading and ALL existing lines
|
|
861
|
+
// (e.g. prose like "Next recommended action"). Fields already updated in
|
|
862
|
+
// place above (needs* false) are not re-inserted. A function replacement
|
|
863
|
+
// is used so `$`-bearing caller values are inserted literally (#3454).
|
|
864
|
+
const linesToInsert = [];
|
|
865
|
+
if (needsLastSession)
|
|
866
|
+
linesToInsert.push(`**Last session:** ${now}`);
|
|
867
|
+
if (needsStoppedAt)
|
|
868
|
+
linesToInsert.push(`**Stopped at:** ${stoppedAtValue}`);
|
|
869
|
+
if (needsResumeFile)
|
|
870
|
+
linesToInsert.push(`**Resume file:** ${resumeValue}`);
|
|
871
|
+
if (linesToInsert.length > 0) {
|
|
872
|
+
// Case-insensitive to match the `existingSessionContinuity` detection
|
|
873
|
+
// above (#1101 review F3) — otherwise a lowercase heading would detect
|
|
874
|
+
// but no-op the insert while still reporting the fields as updated.
|
|
875
|
+
content = content.replace(/^(## Session Continuity[ \t]*\n)/im, (_m, heading) => heading + linesToInsert.join('\n') + '\n');
|
|
876
|
+
}
|
|
877
|
+
}
|
|
711
878
|
else {
|
|
712
|
-
// No
|
|
879
|
+
// No session heading exists at all — append a new canonical section.
|
|
713
880
|
const scaffold = [
|
|
714
881
|
'',
|
|
715
882
|
'## Session',
|
|
@@ -741,6 +908,20 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
741
908
|
output({ recorded: false, reason: 'No session fields found in STATE.md' }, raw, 'false');
|
|
742
909
|
}
|
|
743
910
|
}
|
|
911
|
+
/**
|
|
912
|
+
* Match the session section body from a STATE.md body. #1101: recognise the
|
|
913
|
+
* bootstrap `## Session Continuity` heading but PREFER the normalized `## Session`
|
|
914
|
+
* block when both exist (legacy duplicate files), so the reader agrees with the
|
|
915
|
+
* writer (which updates `## Session` first). `(?:^|\n)` line-anchors (kept out of
|
|
916
|
+
* `/m` so `$` stays end-of-string for the `(?=\n##|$)` section boundary), which
|
|
917
|
+
* excludes an h3 `### Session Continuity`; the trailing-` Archive` boundary still
|
|
918
|
+
* excludes `## Session Continuity Archive` (preserving the #2444 scoping).
|
|
919
|
+
* Returns the match whose group 1 is the section body, or null.
|
|
920
|
+
*/
|
|
921
|
+
function matchSessionSection(body) {
|
|
922
|
+
return body.match(/(?:^|\n)##[ \t]*Session[ \t]*\n([\s\S]*?)(?=\n##|$)/i)
|
|
923
|
+
|| body.match(/(?:^|\n)##[ \t]*Session Continuity[ \t]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
924
|
+
}
|
|
744
925
|
function cmdStateSnapshot(cwd, raw) {
|
|
745
926
|
const statePath = planningPaths(cwd).state;
|
|
746
927
|
if (!node_fs_1.default.existsSync(statePath)) {
|
|
@@ -816,7 +997,9 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
816
997
|
stopped_at: null,
|
|
817
998
|
resume_file: null,
|
|
818
999
|
};
|
|
819
|
-
|
|
1000
|
+
// #1101: prefer the canonical `## Session` block, falling back to the bootstrap
|
|
1001
|
+
// `## Session Continuity` heading. See matchSessionSection for the anchoring.
|
|
1002
|
+
const sessionMatch = matchSessionSection(body);
|
|
820
1003
|
if (sessionMatch) {
|
|
821
1004
|
const sessionSection = sessionMatch[1];
|
|
822
1005
|
// Accept both `**Last Date:**` (canonical template form) and `**Last session:**`
|
|
@@ -872,7 +1055,9 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
872
1055
|
// historical "Stopped at:" prose elsewhere in the body (e.g. in a
|
|
873
1056
|
// Session Continuity Archive section) never overwrites the current value.
|
|
874
1057
|
// Fall back to full-body search only when no ## Session section exists.
|
|
875
|
-
|
|
1058
|
+
// #1101: prefer the canonical `## Session` block, falling back to the bootstrap
|
|
1059
|
+
// `## Session Continuity` heading. See matchSessionSection for the anchoring.
|
|
1060
|
+
const sessionSectionMatch = matchSessionSection(bodyContent);
|
|
876
1061
|
const sessionBodyScope = sessionSectionMatch ? sessionSectionMatch[1] : bodyContent;
|
|
877
1062
|
const stoppedAt = (0, state_document_cjs_1.stateExtractField)(sessionBodyScope, 'Stopped At') || (0, state_document_cjs_1.stateExtractField)(sessionBodyScope, 'Stopped at');
|
|
878
1063
|
const pausedAt = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Paused At');
|
|
@@ -2146,6 +2331,7 @@ module.exports = {
|
|
|
2146
2331
|
cmdStateUpdateProgress,
|
|
2147
2332
|
cmdStateAddDecision,
|
|
2148
2333
|
cmdStateAddBlocker,
|
|
2334
|
+
cmdStateAddRoadmapEvolution,
|
|
2149
2335
|
cmdStateResolveBlocker,
|
|
2150
2336
|
cmdStateRecordSession,
|
|
2151
2337
|
cmdStateSnapshot,
|
|
@@ -1352,7 +1352,7 @@ function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
|
|
|
1352
1352
|
const pDir = planningDir(cwd);
|
|
1353
1353
|
const phasesDir = node_path_1.default.join(pDir, 'phases');
|
|
1354
1354
|
if (!node_fs_1.default.existsSync(phasesDir)) {
|
|
1355
|
-
output({ drift_detected: false, blocking: false, message: 'No phases directory' }, raw);
|
|
1355
|
+
output({ block: false, drift_detected: false, blocking: false, message: 'No phases directory' }, raw);
|
|
1356
1356
|
return;
|
|
1357
1357
|
}
|
|
1358
1358
|
let phaseDir = null;
|
|
@@ -1369,7 +1369,7 @@ function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
|
|
|
1369
1369
|
phaseDir = exact;
|
|
1370
1370
|
}
|
|
1371
1371
|
if (!phaseDir) {
|
|
1372
|
-
output({ drift_detected: false, blocking: false, message: `Phase directory not found: ${phaseArg}` }, raw);
|
|
1372
|
+
output({ block: false, drift_detected: false, blocking: false, message: `Phase directory not found: ${phaseArg}` }, raw);
|
|
1373
1373
|
return;
|
|
1374
1374
|
}
|
|
1375
1375
|
const allFiles = [];
|
|
@@ -1392,14 +1392,20 @@ function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
|
|
|
1392
1392
|
executionLog += '\n' + gitLog.stdout;
|
|
1393
1393
|
}
|
|
1394
1394
|
const result = (0, schema_detect_cjs_1.checkSchemaDrift)(allFiles, executionLog, { skipCheck: !!skipFlag });
|
|
1395
|
+
const isSkipped = !!result['skipped'];
|
|
1395
1396
|
output({
|
|
1397
|
+
// Uniform gate contract: `block` = true means "this gate's bad condition is met".
|
|
1398
|
+
// When skipCheck is true (GSD_SKIP_SCHEMA_CHECK=true), the gate is bypassed —
|
|
1399
|
+
// block must be false regardless of whether drift was detected.
|
|
1400
|
+
// drift_detected and blocking are kept for compatibility.
|
|
1401
|
+
block: isSkipped ? false : !!result['driftDetected'],
|
|
1396
1402
|
drift_detected: result['driftDetected'],
|
|
1397
1403
|
blocking: result['blocking'],
|
|
1398
1404
|
schema_files: result['schemaFiles'],
|
|
1399
1405
|
orms: result['orms'],
|
|
1400
1406
|
unpushed_orms: result['unpushedOrms'],
|
|
1401
1407
|
message: result['message'],
|
|
1402
|
-
skipped:
|
|
1408
|
+
skipped: isSkipped,
|
|
1403
1409
|
}, raw);
|
|
1404
1410
|
}
|
|
1405
1411
|
function cmdVerifyCodebaseDrift(cwd, raw) {
|
|
@@ -1412,6 +1418,8 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
|
|
|
1412
1418
|
const structurePath = node_path_1.default.join(codebaseDir, 'STRUCTURE.md');
|
|
1413
1419
|
if (!node_fs_1.default.existsSync(structurePath)) {
|
|
1414
1420
|
emit({
|
|
1421
|
+
// Uniform gate contract: block = action_required (false when skipped).
|
|
1422
|
+
block: false,
|
|
1415
1423
|
skipped: true,
|
|
1416
1424
|
reason: 'no-structure-md',
|
|
1417
1425
|
action_required: false,
|
|
@@ -1426,6 +1434,7 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
|
|
|
1426
1434
|
}
|
|
1427
1435
|
catch (err) {
|
|
1428
1436
|
emit({
|
|
1437
|
+
block: false,
|
|
1429
1438
|
skipped: true,
|
|
1430
1439
|
reason: 'cannot-read-structure-md: ' + (err instanceof Error ? err.message : String(err)),
|
|
1431
1440
|
action_required: false,
|
|
@@ -1438,6 +1447,7 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
|
|
|
1438
1447
|
const revProbe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', 'HEAD'], { cwd });
|
|
1439
1448
|
if (revProbe.exitCode !== 0) {
|
|
1440
1449
|
emit({
|
|
1450
|
+
block: false,
|
|
1441
1451
|
skipped: true,
|
|
1442
1452
|
reason: 'not-a-git-repo',
|
|
1443
1453
|
action_required: false,
|
|
@@ -1459,6 +1469,7 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
|
|
|
1459
1469
|
const diff = (0, shell_command_projection_cjs_1.execGit)(['diff', '--name-status', base, 'HEAD'], { cwd });
|
|
1460
1470
|
if (diff.exitCode !== 0) {
|
|
1461
1471
|
emit({
|
|
1472
|
+
block: false,
|
|
1462
1473
|
skipped: true,
|
|
1463
1474
|
reason: 'git-diff-failed',
|
|
1464
1475
|
action_required: false,
|
|
@@ -1500,10 +1511,13 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
|
|
|
1500
1511
|
action,
|
|
1501
1512
|
runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd),
|
|
1502
1513
|
});
|
|
1514
|
+
const actionRequired = !!driftResult['actionRequired'];
|
|
1503
1515
|
emit({
|
|
1516
|
+
// Uniform gate contract: block = action_required.
|
|
1517
|
+
block: actionRequired,
|
|
1504
1518
|
skipped: !!driftResult['skipped'],
|
|
1505
1519
|
reason: driftResult['reason'] || null,
|
|
1506
|
-
action_required:
|
|
1520
|
+
action_required: actionRequired,
|
|
1507
1521
|
directive: driftResult['directive'],
|
|
1508
1522
|
spawn_mapper: !!driftResult['spawnMapper'],
|
|
1509
1523
|
affected_paths: driftResult['affectedPaths'] || [],
|
|
@@ -1516,6 +1530,7 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
|
|
|
1516
1530
|
}
|
|
1517
1531
|
catch (err) {
|
|
1518
1532
|
emit({
|
|
1533
|
+
block: false,
|
|
1519
1534
|
skipped: true,
|
|
1520
1535
|
reason: 'exception: ' + (err && err instanceof Error ? err.message : String(err)),
|
|
1521
1536
|
action_required: false,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"project_code": null,
|
|
14
14
|
"phase_id_convention": null,
|
|
15
15
|
"mode": "interactive",
|
|
16
|
-
"claude_md_path": "
|
|
16
|
+
"claude_md_path": "./.claude/CLAUDE.md",
|
|
17
17
|
"git": {
|
|
18
18
|
"branching_strategy": "none",
|
|
19
19
|
"create_tag": true,
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
"verifier": true,
|
|
29
29
|
"nyquist_validation": true,
|
|
30
30
|
"ai_integration_phase": true,
|
|
31
|
-
"tdd_mode": false,
|
|
32
31
|
"human_verify_mode": "end-of-phase",
|
|
33
32
|
"auto_advance": false,
|
|
34
33
|
"_auto_chain_active": false,
|
|
@@ -10,17 +10,11 @@
|
|
|
10
10
|
"brave_search",
|
|
11
11
|
"firecrawl",
|
|
12
12
|
"exa_search",
|
|
13
|
-
"workflow.research",
|
|
14
13
|
"workflow.plan_check",
|
|
15
14
|
"workflow.verifier",
|
|
16
|
-
"workflow.nyquist_validation",
|
|
17
|
-
"workflow.ai_integration_phase",
|
|
18
|
-
"workflow.ui_phase",
|
|
19
|
-
"workflow.ui_safety_gate",
|
|
20
15
|
"workflow.auto_advance",
|
|
21
16
|
"workflow.node_repair",
|
|
22
17
|
"workflow.node_repair_budget",
|
|
23
|
-
"workflow.tdd_mode",
|
|
24
18
|
"workflow.human_verify_mode",
|
|
25
19
|
"workflow.text_mode",
|
|
26
20
|
"workflow.research_before_questions",
|
|
@@ -29,21 +23,12 @@
|
|
|
29
23
|
"workflow.auto_prune_state",
|
|
30
24
|
"workflow.use_worktrees",
|
|
31
25
|
"workflow.worktree_skip_hooks",
|
|
32
|
-
"workflow.code_review",
|
|
33
|
-
"workflow.code_review_depth",
|
|
34
26
|
"workflow.code_review_command",
|
|
35
|
-
"workflow.pattern_mapper",
|
|
36
27
|
"workflow.plan_bounce",
|
|
37
28
|
"workflow.plan_bounce_script",
|
|
38
29
|
"workflow.plan_bounce_passes",
|
|
39
30
|
"workflow.plan_chunked",
|
|
40
31
|
"workflow.plan_review_convergence",
|
|
41
|
-
"workflow.post_planning_gaps",
|
|
42
|
-
"workflow.security_enforcement",
|
|
43
|
-
"workflow.security_asvs_level",
|
|
44
|
-
"workflow.security_block_on",
|
|
45
|
-
"workflow.drift_threshold",
|
|
46
|
-
"workflow.drift_action",
|
|
47
32
|
"code_quality.fallow.enabled",
|
|
48
33
|
"code_quality.fallow.scope",
|
|
49
34
|
"code_quality.fallow.profile",
|
|
@@ -76,7 +61,6 @@
|
|
|
76
61
|
"workflow.context_coverage_gate",
|
|
77
62
|
"statusline.show_last_command",
|
|
78
63
|
"statusline.context_position",
|
|
79
|
-
"workflow.ui_review",
|
|
80
64
|
"workflow.max_discuss_passes",
|
|
81
65
|
"features.thinking_partner",
|
|
82
66
|
"context",
|
|
@@ -90,8 +74,6 @@
|
|
|
90
74
|
"manager.flags.execute",
|
|
91
75
|
"response_language",
|
|
92
76
|
"context_window",
|
|
93
|
-
"intel.enabled",
|
|
94
|
-
"graphify.enabled",
|
|
95
77
|
"graphify.build_timeout",
|
|
96
78
|
"graphify.auto_update",
|
|
97
79
|
"claude_md_path",
|
|
@@ -153,6 +153,7 @@
|
|
|
153
153
|
"gsd-eval-planner": { "golden": "opus", "balanced": "opus", "budget": "sonnet", "phaseType": "planning", "routingTier": "heavy" },
|
|
154
154
|
"gsd-framework-selector": { "golden": "opus", "balanced": "sonnet", "budget": "sonnet", "phaseType": "planning", "routingTier": "heavy" },
|
|
155
155
|
"gsd-intel-updater": { "golden": "opus", "balanced": "sonnet", "budget": "haiku", "phaseType": "research", "routingTier": "light" },
|
|
156
|
+
"gsd-mempalace-curator": { "golden": "sonnet", "balanced": "sonnet", "budget": "haiku", "phaseType": "research", "routingTier": "light" },
|
|
156
157
|
"gsd-security-auditor": { "golden": "opus", "balanced": "sonnet", "budget": "sonnet", "phaseType": "verification", "routingTier": "heavy" },
|
|
157
158
|
"gsd-user-profiler": { "golden": "opus", "balanced": "sonnet", "budget": "sonnet", "phaseType": "research", "routingTier": "heavy" }
|
|
158
159
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Loop Hook Dispatch Contract
|
|
2
|
+
|
|
3
|
+
Generic reference for consuming the `--raw` JSON output of `gsd_run loop render-hooks <point>`
|
|
4
|
+
in any host-loop workflow. This document is point-agnostic — it applies to every loop
|
|
5
|
+
extension point (discuss:pre, discuss:post, plan:pre, plan:post, execute:pre, execute:wave:pre,
|
|
6
|
+
execute:wave:post, execute:post, verify:pre, verify:post, ship:pre, ship:post).
|
|
7
|
+
|
|
8
|
+
## Envelope shape
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"point": "discuss:pre",
|
|
13
|
+
"activeHooks": [
|
|
14
|
+
{ "kind": "contribution", "into": "orchestrator", "fragment": { "inline": "..." } },
|
|
15
|
+
{ "kind": "step", "ref": { "skill": "my-skill" } },
|
|
16
|
+
{ "kind": "gate", "check": { "query": "..." }, "blocking": true, "onError": "skip" }
|
|
17
|
+
],
|
|
18
|
+
"rendered": "..."
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`activeHooks` is an array of enabled hook entries for the named point. It is empty (or absent)
|
|
23
|
+
when no capability has registered an active hook at this point — treat that as a no-op.
|
|
24
|
+
|
|
25
|
+
## Dispatch rules by `kind`
|
|
26
|
+
|
|
27
|
+
### `contribution`
|
|
28
|
+
|
|
29
|
+
Inject `fragment.inline` verbatim into the context for the role named in `into`
|
|
30
|
+
(e.g. `orchestrator`, `planner`). Do not paraphrase — the text is the product.
|
|
31
|
+
|
|
32
|
+
### `step`
|
|
33
|
+
|
|
34
|
+
Dispatch the referenced unit:
|
|
35
|
+
|
|
36
|
+
- `ref.skill` present → dispatch via the Skill tool with skill id `gsd-<ref.skill>`.
|
|
37
|
+
- `ref.agent` present → dispatch via the Agent tool with `subagent_type` = `ref.agent`.
|
|
38
|
+
Before dispatching an agent, print the canonical liveness banner so users know silence
|
|
39
|
+
is expected and do not kill a healthy agent:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
◆ Spawning <agent>... (runs in a subagent — no output until it returns; expected, not a freeze)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Wait for the result before continuing to the next hook or the next step.
|
|
46
|
+
|
|
47
|
+
### `gate`
|
|
48
|
+
|
|
49
|
+
Evaluate `check` (one of `query`, `predicate`, or `agentVerdict`). Then honor `blocking`:
|
|
50
|
+
|
|
51
|
+
- `blocking: true` → if the check returns `block: true`, surface `check.message` to the user
|
|
52
|
+
and stop the current step. Do not continue.
|
|
53
|
+
- `blocking: false` → advisory only; surface the message but continue regardless of outcome.
|
|
54
|
+
|
|
55
|
+
Honor `onError` if the check itself errors: `skip` means treat as non-blocking and continue;
|
|
56
|
+
`fail` means surface the error and stop.
|
|
57
|
+
|
|
58
|
+
## Empty / absent `activeHooks`
|
|
59
|
+
|
|
60
|
+
If `activeHooks` is absent, null, or an empty array, skip silently and continue to the next
|
|
61
|
+
step in the workflow. No output to the user is needed.
|
|
@@ -143,13 +143,15 @@ For each REQ-ID, determine status using all three sources:
|
|
|
143
143
|
|
|
144
144
|
## 5.5. Nyquist Compliance Discovery
|
|
145
145
|
|
|
146
|
-
Skip if
|
|
146
|
+
Skip if the Nyquist capability is inactive.
|
|
147
147
|
|
|
148
148
|
```bash
|
|
149
|
-
|
|
149
|
+
VERIFY_POST_HOOKS_JSON=$(gsd_run loop render-hooks verify:post --raw)
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "validate-phase"`.
|
|
153
|
+
|
|
154
|
+
If no active validate-phase step hook exists: skip entirely.
|
|
153
155
|
|
|
154
156
|
For each phase directory, check `*-VALIDATION.md`. If exists, parse frontmatter (`nyquist_compliant`, `wave_0_complete`).
|
|
155
157
|
|
|
@@ -453,17 +453,22 @@ Skill(skill="gsd-execute-phase", args="${PHASE_NUM} --no-transition")
|
|
|
453
453
|
|
|
454
454
|
Auto-invoke code review and fix chain. Autonomous mode chains both review and fix (unlike execute-phase/quick which only suggest fix).
|
|
455
455
|
|
|
456
|
-
**
|
|
456
|
+
**Capability dispatch:**
|
|
457
457
|
```bash
|
|
458
|
-
|
|
458
|
+
EXECUTE_POST_HOOKS_JSON=$(gsd_run loop render-hooks execute:post --raw)
|
|
459
459
|
```
|
|
460
|
-
|
|
460
|
+
|
|
461
|
+
Resolve active step hooks from `EXECUTE_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "code-review"`.
|
|
462
|
+
|
|
463
|
+
If no active code-review step hook exists: display "Code review skipped (code-review capability inactive)" and proceed to 3d. This covers `workflow.code_review=false` through the Capability Registry; do not query the code-review toggle directly here.
|
|
464
|
+
|
|
465
|
+
For each active code-review step hook, dispatch the skill using the registry-provided stem:
|
|
461
466
|
|
|
462
467
|
```
|
|
463
|
-
Skill(skill="gsd
|
|
468
|
+
Skill(skill="gsd-${ref.skill}", args="${PHASE_NUM}")
|
|
464
469
|
```
|
|
465
470
|
|
|
466
|
-
Parse status from REVIEW.md frontmatter. If "clean" or "skipped": proceed to 3d. If findings found
|
|
471
|
+
Parse status from REVIEW.md frontmatter. If "clean" or "skipped": proceed to 3d. If findings found after the capability-dispatched review, auto-invoke the consolidated fix entry point:
|
|
467
472
|
```
|
|
468
473
|
Skill(skill="gsd-code-review", args="${PHASE_NUM} --fix --auto")
|
|
469
474
|
```
|