@opengsd/gsd-core 1.8.0 → 1.9.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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +31 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +36 -0
- package/agents/gsd-executor.md +20 -7
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +15 -23
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/bin/install.js +186 -55
- package/commands/gsd/plan-review-convergence.md +5 -1
- package/gsd-core/bin/gsd-tools.cjs +849 -2
- package/gsd-core/bin/lib/api-coverage.cjs +22 -8
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/check-command-router.cjs +12 -2
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
- package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
- package/gsd-core/bin/lib/commands.cjs +246 -18
- package/gsd-core/bin/lib/config-loader.cjs +200 -28
- package/gsd-core/bin/lib/config.cjs +90 -5
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +44 -19
- package/gsd-core/bin/lib/install-engine.cjs +1 -0
- package/gsd-core/bin/lib/milestone.cjs +5 -5
- package/gsd-core/bin/lib/model-catalog.cjs +51 -1
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +278 -5
- package/gsd-core/bin/lib/phase.cjs +57 -5
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +1 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
- package/gsd-core/bin/lib/roadmap.cjs +10 -4
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +1 -1
- package/gsd-core/bin/lib/state-document.cjs +164 -20
- package/gsd-core/bin/lib/state-transition.cjs +28 -10
- package/gsd-core/bin/lib/state.cjs +141 -21
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +9 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +32 -0
- package/gsd-core/bin/lib/verification.cjs +51 -14
- package/gsd-core/bin/lib/verify.cjs +128 -20
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -21
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/ai-integration-phase.md +4 -4
- package/gsd-core/workflows/audit-fix.md +4 -0
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +76 -19
- package/gsd-core/workflows/debug.md +10 -2
- package/gsd-core/workflows/diagnose-issues.md +4 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/docs-update.md +8 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase.md +85 -115
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/help/modes/full.md +3 -3
- package/gsd-core/workflows/import.md +4 -1
- package/gsd-core/workflows/ingest-docs.md +4 -0
- package/gsd-core/workflows/map-codebase.md +13 -6
- package/gsd-core/workflows/new-milestone.md +10 -2
- package/gsd-core/workflows/new-project.md +11 -4
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/plan-phase.md +42 -46
- package/gsd-core/workflows/plan-review-convergence.md +18 -14
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +14 -3
- package/gsd-core/workflows/review.md +146 -575
- package/gsd-core/workflows/scan.md +9 -1
- package/gsd-core/workflows/secure-phase.md +10 -2
- package/gsd-core/workflows/ship.md +41 -11
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/ui-phase.md +8 -1
- package/gsd-core/workflows/ui-review.md +8 -1
- package/gsd-core/workflows/update.md +104 -5
- package/gsd-core/workflows/validate-phase.md +10 -2
- package/gsd-core/workflows/verify-work.md +8 -1
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +9 -6
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +9 -6
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +7 -7
- package/pi/gsd.cjs +26 -1
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +24 -0
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +6 -4
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -562,13 +562,19 @@ function parseCoverageMatrix(text) {
|
|
|
562
562
|
}
|
|
563
563
|
return out;
|
|
564
564
|
}
|
|
565
|
-
// (2) markdown table — collect
|
|
565
|
+
// (2) markdown table — collect rows from coverage matrix tables only (#2366).
|
|
566
|
+
// Track whether we are inside a recognized coverage matrix (after a header
|
|
567
|
+
// row, before a non-pipe line ends the table). This prevents summary tables
|
|
568
|
+
// elsewhere in the file from being parsed as data (#2366 bug 1) and allows
|
|
569
|
+
// multi-section matrices with repeated headers (#2366 bug 2).
|
|
566
570
|
const lines = src.split('\n');
|
|
567
|
-
let
|
|
571
|
+
let inMatrix = false;
|
|
568
572
|
for (const line of lines) {
|
|
569
573
|
const trimmed = line.trim();
|
|
570
|
-
if (!trimmed.startsWith('|'))
|
|
574
|
+
if (!trimmed.startsWith('|')) {
|
|
575
|
+
inMatrix = false;
|
|
571
576
|
continue;
|
|
577
|
+
}
|
|
572
578
|
const cells = trimmed.slice(1, trimmed.endsWith('|') ? -1 : trimmed.length).split('|');
|
|
573
579
|
if (cells.length < 2)
|
|
574
580
|
continue;
|
|
@@ -577,13 +583,21 @@ function parseCoverageMatrix(text) {
|
|
|
577
583
|
// is not mistaken for a separator.
|
|
578
584
|
if (cleaned.every((c) => /^:?-{3,}:?$/.test(c)))
|
|
579
585
|
continue;
|
|
580
|
-
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
586
|
+
// Strip markdown emphasis (**, *, __, _, `) from the decision cell before
|
|
587
|
+
// comparison so **OPT-OUT** parses correctly (#2366 bug 3).
|
|
588
|
+
const decisionCell = (cleaned[1] || '').replace(/[*_`]/g, '').trim().toUpperCase();
|
|
589
|
+
// header detection — recognized by 'capability' in column 0; allows multiple
|
|
590
|
+
// headers for multi-section matrices (#2366 bug 2).
|
|
591
|
+
if (cleaned[0].toLowerCase() === 'capability') {
|
|
592
|
+
inMatrix = true;
|
|
593
|
+
if (out.format === 'none')
|
|
594
|
+
out.format = 'table';
|
|
585
595
|
continue;
|
|
586
596
|
}
|
|
597
|
+
// Only parse data rows from inside a recognized coverage matrix table.
|
|
598
|
+
// A pipe-table outside the matrix (e.g., a summary table) is ignored (#2366 bug 1).
|
|
599
|
+
if (!inMatrix)
|
|
600
|
+
continue;
|
|
587
601
|
if (!VALID_DECISIONS.has(decisionCell)) {
|
|
588
602
|
// A row that otherwise looks like data (≥3 cells, non-empty capability)
|
|
589
603
|
// but carries a malformed decision is a real error, not a row to skip
|
|
@@ -66,7 +66,7 @@ function scanDebugSessions(planDir) {
|
|
|
66
66
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
|
|
67
67
|
if (content === null)
|
|
68
68
|
continue;
|
|
69
|
-
const fm = extractFrontmatter(content);
|
|
69
|
+
const fm = extractFrontmatter(content, safeFilePath);
|
|
70
70
|
const status = (fm.status || 'unknown').toLowerCase();
|
|
71
71
|
if (status === 'resolved' || status === 'complete')
|
|
72
72
|
continue;
|
|
@@ -148,7 +148,7 @@ function scanQuickTasks(planDir) {
|
|
|
148
148
|
status = 'unreadable';
|
|
149
149
|
}
|
|
150
150
|
else {
|
|
151
|
-
const fm = extractFrontmatter(content);
|
|
151
|
+
const fm = extractFrontmatter(content, safeSum);
|
|
152
152
|
status = (fm.status || 'unknown').toLowerCase();
|
|
153
153
|
}
|
|
154
154
|
}
|
|
@@ -205,7 +205,7 @@ function scanThreads(planDir) {
|
|
|
205
205
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
|
|
206
206
|
if (content === null)
|
|
207
207
|
continue;
|
|
208
|
-
const fm = extractFrontmatter(content);
|
|
208
|
+
const fm = extractFrontmatter(content, safeFilePath);
|
|
209
209
|
let status = (fm.status || '').toLowerCase().trim();
|
|
210
210
|
// Fall back to scanning body for ## Status: OPEN / IN PROGRESS
|
|
211
211
|
if (!status) {
|
|
@@ -266,7 +266,7 @@ function scanTodos(planDir) {
|
|
|
266
266
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
|
|
267
267
|
if (content === null)
|
|
268
268
|
continue;
|
|
269
|
-
const fm = extractFrontmatter(content);
|
|
269
|
+
const fm = extractFrontmatter(content, safeFilePath);
|
|
270
270
|
// Extract first line of body after frontmatter
|
|
271
271
|
const bodyMatch = content.replace(/^---[\s\S]*?---\n?/, '');
|
|
272
272
|
const firstLine = bodyMatch.trim().split('\n')[0] || '';
|
|
@@ -317,7 +317,7 @@ function scanSeeds(planDir) {
|
|
|
317
317
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
|
|
318
318
|
if (content === null)
|
|
319
319
|
continue;
|
|
320
|
-
const fm = extractFrontmatter(content);
|
|
320
|
+
const fm = extractFrontmatter(content, safeFilePath);
|
|
321
321
|
const status = (fm.status || 'dormant').toLowerCase();
|
|
322
322
|
if (!unimplementedStatuses.has(status))
|
|
323
323
|
continue;
|
|
@@ -382,7 +382,7 @@ function scanUatGaps(planDir) {
|
|
|
382
382
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
|
|
383
383
|
if (content === null)
|
|
384
384
|
continue;
|
|
385
|
-
const fm = extractFrontmatter(content);
|
|
385
|
+
const fm = extractFrontmatter(content, safeFilePath);
|
|
386
386
|
const status = (fm.status || 'unknown').toLowerCase();
|
|
387
387
|
const result = (fm.result || '').toLowerCase();
|
|
388
388
|
// Also accept `result: all_pass` as a fallback when status is absent
|
|
@@ -445,7 +445,7 @@ function scanVerificationGaps(planDir) {
|
|
|
445
445
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
|
|
446
446
|
if (content === null)
|
|
447
447
|
continue;
|
|
448
|
-
const fm = extractFrontmatter(content);
|
|
448
|
+
const fm = extractFrontmatter(content, safeFilePath);
|
|
449
449
|
const status = (fm.status || 'unknown').toLowerCase();
|
|
450
450
|
if (status !== 'gaps_found' && status !== 'human_needed')
|
|
451
451
|
continue;
|
|
@@ -500,7 +500,7 @@ function scanContextQuestions(planDir) {
|
|
|
500
500
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
|
|
501
501
|
if (content === null)
|
|
502
502
|
continue;
|
|
503
|
-
const fm = extractFrontmatter(content);
|
|
503
|
+
const fm = extractFrontmatter(content, safeFilePath);
|
|
504
504
|
// Check frontmatter open_questions field
|
|
505
505
|
let questions = [];
|
|
506
506
|
if (fm.open_questions) {
|
|
@@ -399,6 +399,10 @@ function isValidConsentRecord(rec) {
|
|
|
399
399
|
return false;
|
|
400
400
|
if (typeof r['consentedAt'] !== 'string' || !r['consentedAt'])
|
|
401
401
|
return false;
|
|
402
|
+
// Optional (see ConsentRecord.reviewerHost): absent is valid and is the common case. Present but
|
|
403
|
+
// non-string is a corrupt record — reject rather than silently comparing against a non-host.
|
|
404
|
+
if (r['reviewerHost'] !== undefined && typeof r['reviewerHost'] !== 'string')
|
|
405
|
+
return false;
|
|
402
406
|
return true;
|
|
403
407
|
}
|
|
404
408
|
/**
|
|
@@ -645,7 +649,7 @@ function writeConsentStore(gsdHome, store) {
|
|
|
645
649
|
* written. (The OLD code returned a null handle and proceeded unlocked — that is the bug.)
|
|
646
650
|
*/
|
|
647
651
|
function recordProjectConsent(args) {
|
|
648
|
-
const { gsdHome, projectRoot, id, integrity, disclosureSignature, contentHash } = args;
|
|
652
|
+
const { gsdHome, projectRoot, id, integrity, disclosureSignature, contentHash, reviewerHost } = args;
|
|
649
653
|
if (isUnsafeCapabilityId(id)) {
|
|
650
654
|
throw new Error(`Invalid capability id "${String(id)}": must match /^[a-z][a-z0-9-]*$/ (kebab-case, lowercase). ` +
|
|
651
655
|
`Unsafe or non-kebab ids are rejected to keep the consent store prototype-pollution-safe.`);
|
|
@@ -685,6 +689,9 @@ function recordProjectConsent(args) {
|
|
|
685
689
|
disclosureSignature,
|
|
686
690
|
contentHash,
|
|
687
691
|
consentedAt: new Date().toISOString(),
|
|
692
|
+
// Written only when the capability actually declares an egress destination, so a spawn lane
|
|
693
|
+
// or a non-lane capability keeps a byte-identical record shape.
|
|
694
|
+
...(typeof reviewerHost === 'string' && reviewerHost ? { reviewerHost } : {}),
|
|
688
695
|
};
|
|
689
696
|
writeConsentStore(gsdHome, store);
|
|
690
697
|
}
|
|
@@ -730,6 +737,37 @@ function revokeProjectConsent(args) {
|
|
|
730
737
|
releaseConsentLock(lock);
|
|
731
738
|
}
|
|
732
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* The egress destination a capability's consent was granted against, or `undefined`.
|
|
742
|
+
*
|
|
743
|
+
* ADR-2782 D5 rule 4 (#2799) — read on the INVOCATION path, not only at install, because
|
|
744
|
+
* `hostConfigKey` names a key in `.planning/config.json`: the one consent-bound value that lives
|
|
745
|
+
* outside the SHA-pinned bundle and can be changed by an ordinary pull request with no re-install
|
|
746
|
+
* and no integrity check.
|
|
747
|
+
*
|
|
748
|
+
* `undefined` means "nothing to compare", and the caller MUST treat that as allow, not deny. Two
|
|
749
|
+
* legitimate ways to get it: the capability has no consent record at all (first-party lanes ship
|
|
750
|
+
* inside the SHA-pinned distribution and are never consent-gated), or the record predates this
|
|
751
|
+
* field. Denying on absence would break every existing local-model user on upgrade.
|
|
752
|
+
*
|
|
753
|
+
* Non-throwing: a missing, corrupt, or oversized store yields `undefined` like any other absence.
|
|
754
|
+
*/
|
|
755
|
+
function readConsentedReviewerHost(args) {
|
|
756
|
+
const { gsdHome, projectRoot, id } = args;
|
|
757
|
+
if (isUnsafeCapabilityId(id))
|
|
758
|
+
return undefined;
|
|
759
|
+
try {
|
|
760
|
+
const store = readConsentStore(gsdHome);
|
|
761
|
+
const key = consentKey(realpathProject(projectRoot), id);
|
|
762
|
+
if (!Object.prototype.hasOwnProperty.call(store.records, key))
|
|
763
|
+
return undefined;
|
|
764
|
+
const host = store.records[key].reviewerHost;
|
|
765
|
+
return typeof host === 'string' && host ? host : undefined;
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
return undefined;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
733
771
|
/**
|
|
734
772
|
* #1459 finding 2 (round 6): TEST-ONLY — override the cumulative bundle entry-count cap and return a
|
|
735
773
|
* restore() that resets it to the production default. Lets a test prove the streaming walk fails closed
|
|
@@ -744,6 +782,7 @@ module.exports = {
|
|
|
744
782
|
consentStorePath,
|
|
745
783
|
bundleContentHash,
|
|
746
784
|
readConsentStore,
|
|
785
|
+
readConsentedReviewerHost,
|
|
747
786
|
hasProjectConsent,
|
|
748
787
|
recordProjectConsent,
|
|
749
788
|
revokeProjectConsent,
|
|
@@ -672,6 +672,59 @@ function warnIfConsentSkipped(opts, id) {
|
|
|
672
672
|
* Best-effort: a consent-store write failure must not turn a successful install/upgrade into a
|
|
673
673
|
* failure (the bundle is already committed) — it is surfaced as a warning, not a throw.
|
|
674
674
|
*/
|
|
675
|
+
/**
|
|
676
|
+
* Resolve an `openai-http` reviewer lane's declared `hostConfigKey` to the destination it currently
|
|
677
|
+
* names, or `undefined` when this capability is not such a lane.
|
|
678
|
+
*
|
|
679
|
+
* Falls back to the lane's declared `defaultHost` when the key is unset, because that is exactly
|
|
680
|
+
* what the invocation path will do — binding the config value while the runtime uses the default
|
|
681
|
+
* would guarantee a mismatch on the very first review.
|
|
682
|
+
*
|
|
683
|
+
* Non-throwing: consent binding is best-effort and must never turn a successful install into a
|
|
684
|
+
* failure. An unresolvable host simply records nothing, which reads as "not bound" and allows.
|
|
685
|
+
*/
|
|
686
|
+
function resolveReviewerEgressHost(opts, manifest) {
|
|
687
|
+
try {
|
|
688
|
+
const reviewer = manifest['reviewer'];
|
|
689
|
+
if (reviewer === null || typeof reviewer !== 'object' || Array.isArray(reviewer))
|
|
690
|
+
return undefined;
|
|
691
|
+
const r = reviewer;
|
|
692
|
+
if (r['transport'] !== 'openai-http')
|
|
693
|
+
return undefined;
|
|
694
|
+
const invoke = r['invoke'];
|
|
695
|
+
if (invoke === null || typeof invoke !== 'object')
|
|
696
|
+
return undefined;
|
|
697
|
+
const inv = invoke;
|
|
698
|
+
const key = typeof inv['hostConfigKey'] === 'string' ? inv['hostConfigKey'] : '';
|
|
699
|
+
const fallback = typeof inv['defaultHost'] === 'string' ? inv['defaultHost'] : '';
|
|
700
|
+
let configured = '';
|
|
701
|
+
if (key) {
|
|
702
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
703
|
+
const cfgLoader = require('./config-loader.cjs');
|
|
704
|
+
const root = projectRootMod.consentProjectRoot(opts.runtimeDir);
|
|
705
|
+
const cfg = cfgLoader.loadConfigResolved ? (cfgLoader.loadConfigResolved(root).config ?? {}) : {};
|
|
706
|
+
let cur = cfg;
|
|
707
|
+
for (const part of key.split('.')) {
|
|
708
|
+
if (cur === null || typeof cur !== 'object') {
|
|
709
|
+
cur = undefined;
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
cur = Object.prototype.hasOwnProperty.call(cur, part)
|
|
713
|
+
? cur[part]
|
|
714
|
+
: undefined;
|
|
715
|
+
}
|
|
716
|
+
if (typeof cur === 'string')
|
|
717
|
+
configured = cur.trim();
|
|
718
|
+
}
|
|
719
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
720
|
+
const { normalizeHost } = require('./review-lane-invocation.cjs');
|
|
721
|
+
const resolved = normalizeHost(configured || fallback);
|
|
722
|
+
return resolved || undefined;
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
675
728
|
function bindProjectConsent(opts, id, integrity, manifest) {
|
|
676
729
|
// #1459 IC-07: a project-scope op WITHOUT a consent store cannot bind — warn (then nothing to do).
|
|
677
730
|
if (!shouldBindConsent(opts)) {
|
|
@@ -689,6 +742,11 @@ function bindProjectConsent(opts, id, integrity, manifest) {
|
|
|
689
742
|
integrity,
|
|
690
743
|
disclosureSignature: trustMod.signatureForManifest(manifest),
|
|
691
744
|
contentHash: consentMod.bundleContentHash(capDir(opts.runtimeDir, id)),
|
|
745
|
+
// ADR-2782 D5 rule 1 (#2799): bind the RESOLVED egress destination, not merely the config key
|
|
746
|
+
// that names it. The key lives in `.planning/config.json`, outside the SHA-pinned bundle, so
|
|
747
|
+
// without this the user consents to "wherever that key points" — a promise the bundle hash
|
|
748
|
+
// cannot keep. Phase 5b re-resolves and compares at invocation (rule 4).
|
|
749
|
+
reviewerHost: resolveReviewerEgressHost(opts, manifest),
|
|
692
750
|
});
|
|
693
751
|
}
|
|
694
752
|
catch (err) {
|
|
@@ -426,6 +426,8 @@ function loadRegistry(options = {}) {
|
|
|
426
426
|
// consent the CLI records is the consent the loader checks. The consent store NEVER lives in a repo.
|
|
427
427
|
const gsdHome = options.gsdHome || process.env['GSD_HOME'] || os.homedir();
|
|
428
428
|
const warnings = [];
|
|
429
|
+
// ADR-2782 D4.3 — non-fatal notices for capabilities that are ACCEPTED (see OverlayMeta).
|
|
430
|
+
const diagnostics = [];
|
|
429
431
|
const incompatibleGateCapIds = [];
|
|
430
432
|
const blockedGates = [];
|
|
431
433
|
const commandRoots = {};
|
|
@@ -719,6 +721,26 @@ function loadRegistry(options = {}) {
|
|
|
719
721
|
}
|
|
720
722
|
}
|
|
721
723
|
// Accepted.
|
|
724
|
+
//
|
|
725
|
+
// ADR-2782 D4.3: an unknown field inside a `reviewer` body is IGNORED WITH A
|
|
726
|
+
// WARNING rather than failing validation, so a lane built for a newer GSD
|
|
727
|
+
// degrades to accepted-but-partially-understood instead of being rejected.
|
|
728
|
+
// That case validates cleanly, so without this call it would surface
|
|
729
|
+
// nowhere at runtime — the build-time generator only ever sees first-party
|
|
730
|
+
// in-repo manifests, never an installed third-party overlay. Guarded on
|
|
731
|
+
// presence so an older built validator without the function still loads,
|
|
732
|
+
// and wrapped because the never-crash contract (ADR-1244 D2) outranks a
|
|
733
|
+
// diagnostic: a throwing collector must not cost the user a working lane.
|
|
734
|
+
if (typeof validator.collectReviewerWarnings === 'function') {
|
|
735
|
+
try {
|
|
736
|
+
for (const w of validator.collectReviewerWarnings(cap) || []) {
|
|
737
|
+
diagnostics.push(`${root.scope}:${id}: ${w}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
// A diagnostic that cannot be produced is not worth failing an install over.
|
|
742
|
+
}
|
|
743
|
+
}
|
|
722
744
|
overlayCaps.push(cap);
|
|
723
745
|
acceptedIds.add(id);
|
|
724
746
|
for (const s of skills)
|
|
@@ -752,7 +774,7 @@ function loadRegistry(options = {}) {
|
|
|
752
774
|
}
|
|
753
775
|
}
|
|
754
776
|
}
|
|
755
|
-
const meta = { warnings, incompatibleGateCapIds, blockedGates, commandRoots };
|
|
777
|
+
const meta = { warnings, diagnostics, incompatibleGateCapIds, blockedGates, commandRoots };
|
|
756
778
|
if (overlayCaps.length === 0) {
|
|
757
779
|
// Nothing to compose. Return the frozen registry unchanged when there is
|
|
758
780
|
// also nothing to report (identity-stable); otherwise attach diagnostics.
|