@opengsd/gsd-core 1.4.4 → 1.5.0-rc.2
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/README.md +3 -3
- package/agents/gsd-code-fixer.md +3 -2
- package/agents/gsd-debug-session-manager.md +2 -1
- package/agents/gsd-debugger.md +4 -3
- package/agents/gsd-executor.md +17 -16
- package/agents/gsd-intel-updater.md +38 -41
- package/agents/gsd-phase-researcher.md +8 -8
- package/agents/gsd-plan-checker.md +23 -13
- package/agents/gsd-planner.md +32 -188
- package/agents/gsd-project-researcher.md +5 -4
- package/agents/gsd-research-synthesizer.md +2 -1
- package/agents/gsd-ui-researcher.md +2 -1
- package/agents/gsd-verifier.md +12 -11
- package/bin/install.js +965 -1486
- package/commands/gsd/autonomous.md +5 -1
- package/commands/gsd/ns-manage.md +8 -1
- package/commands/gsd/ns-project.md +5 -0
- package/commands/gsd/ns-review.md +4 -1
- package/commands/gsd/ns-workflow.md +7 -1
- package/commands/gsd/plan-review-convergence.md +5 -4
- package/commands/gsd/surface.md +12 -5
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +198 -101
- package/gsd-core/bin/gsd_run +20 -0
- package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
- package/gsd-core/bin/lib/capability-registry.cjs +2234 -0
- package/gsd-core/bin/lib/capability-state.cjs +336 -0
- package/gsd-core/bin/lib/check-command-router.cjs +133 -2
- package/gsd-core/bin/lib/cli-exit.cjs +22 -3
- package/gsd-core/bin/lib/config-loader.cjs +716 -0
- package/gsd-core/bin/lib/configuration.cjs +4 -34
- package/gsd-core/bin/lib/core-utils.cjs +198 -0
- package/gsd-core/bin/lib/core.cjs +107 -1817
- package/gsd-core/bin/lib/edge-probe.cjs +173 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +63 -25
- package/gsd-core/bin/lib/federated-config.cjs +182 -0
- package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
- package/gsd-core/bin/lib/init.cjs +58 -12
- package/gsd-core/bin/lib/install-profiles.cjs +157 -3
- package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
- package/gsd-core/bin/lib/intel.cjs +3 -3
- package/gsd-core/bin/lib/io.cjs +222 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
- package/gsd-core/bin/lib/model-resolver.cjs +426 -0
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -0
- package/gsd-core/bin/lib/phase-id.cjs +215 -0
- package/gsd-core/bin/lib/phase-locator.cjs +148 -0
- package/gsd-core/bin/lib/phase.cjs +17 -0
- package/gsd-core/bin/lib/probe-core.cjs +257 -0
- package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
- package/gsd-core/bin/lib/roadmap.cjs +44 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +92 -95
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +68 -29
- package/gsd-core/bin/lib/runtime-homes.cjs +163 -87
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1439 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +2 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
- package/gsd-core/bin/lib/state-document.cjs +8 -0
- package/gsd-core/bin/lib/state.cjs +114 -2
- package/gsd-core/bin/lib/surface.cjs +66 -14
- package/gsd-core/bin/lib/uat-predicate.cjs +329 -0
- package/gsd-core/bin/lib/update-context.cjs +4 -1
- package/gsd-core/bin/lib/verify.cjs +104 -1
- package/gsd-core/bin/lib/worktree-base-ref.cjs +33 -8
- package/gsd-core/bin/shared/model-catalog.json +11 -6
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -1
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json +4 -0
- package/gsd-core/references/edge-probe.md +261 -0
- package/gsd-core/references/planner-antipatterns.md +41 -0
- package/gsd-core/references/planner-guidance.md +186 -0
- package/gsd-core/references/planner-reviews.md +5 -2
- package/gsd-core/templates/phase-prompt.md +7 -7
- package/gsd-core/templates/project.md +19 -2
- package/gsd-core/templates/spec.md +12 -0
- package/gsd-core/templates/summary-complex.md +1 -0
- package/gsd-core/templates/summary-minimal.md +1 -0
- package/gsd-core/templates/summary-standard.md +1 -0
- package/gsd-core/templates/summary.md +1 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +111 -51
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +6 -4
- package/gsd-core/workflows/code-review.md +53 -17
- package/gsd-core/workflows/complete-milestone.md +11 -5
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +8 -1
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +8 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +9 -1
- package/gsd-core/workflows/new-project.md +9 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase.md +65 -28
- package/gsd-core/workflows/plan-review-convergence.md +60 -33
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +1 -1
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +7 -7
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/ship.md +8 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +130 -1
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/update.md +2 -2
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +8 -1
- package/package.json +11 -3
- package/scripts/base64-scan.sh +1 -1
- package/scripts/changeset/cli.cjs +8 -1
- package/scripts/changeset/lint.cjs +38 -2
- package/scripts/ci-test-scope.cjs +21 -10
- package/scripts/gen-capability-registry.cjs +2293 -0
- package/scripts/gen-loop-host-contract.cjs +471 -0
- package/scripts/lib/allowlist-ratchet.cjs +101 -1
- package/scripts/lint-regression-test-names.allowlist.json +269 -0
- package/scripts/lint-regression-test-names.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +25 -4
- package/scripts/lint-windows-test-portability.cjs +178 -0
- package/scripts/prompt-injection-scan.sh +4 -4
- package/scripts/research-profiles.cjs +10 -10
- package/scripts/run-tests.cjs +133 -29
- package/scripts/secret-scan.sh +3 -3
- package/scripts/sync-next-version.cjs +133 -0
- package/scripts/sync-runtime-launcher.cjs +21 -5
- package/scripts/update-size-baseline.cjs +68 -0
- package/scripts/workflow-policy.cjs +42 -9
- package/scripts/workflow-size.cjs +90 -0
- package/scripts/run-cross-platform-tests.cjs +0 -67
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* UAT Predicate — Pure-computation UAT pass/fail evaluation
|
|
4
|
+
*
|
|
5
|
+
* Evaluates all *-UAT.md and *-VERIFICATION.md files in a phase directory and
|
|
6
|
+
* returns a typed report. Used by `phase uat-passed` to harden against the
|
|
7
|
+
* naive whole-file regex in cmdPhaseComplete which false-matches `result:` lines
|
|
8
|
+
* inside frontmatter, fenced code blocks, blockquotes, and HTML comments.
|
|
9
|
+
*
|
|
10
|
+
* Issue #247 — phase uat-passed predicate
|
|
11
|
+
*
|
|
12
|
+
* ADR-457 build-at-publish: compiled by tsc to gsd-core/bin/lib/uat-predicate.cjs.
|
|
13
|
+
*/
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
|
+
const frontmatter = require("./frontmatter.cjs");
|
|
21
|
+
const { extractFrontmatter } = frontmatter;
|
|
22
|
+
// ─── Blocking state sets (documented for maintainability) ─────────────────────
|
|
23
|
+
// UAT file frontmatter `status` values that indicate the file is not fully done
|
|
24
|
+
const BLOCKING_UAT_FM_STATUSES = new Set([
|
|
25
|
+
'partial', 'diagnosed', 'pending', 'blocked', 'in_progress', 'failed',
|
|
26
|
+
]);
|
|
27
|
+
// UAT file frontmatter `result` values that indicate failure
|
|
28
|
+
const BLOCKING_UAT_FM_RESULTS = new Set(['pending', 'blocked', 'failed']);
|
|
29
|
+
// VERIFICATION file frontmatter `status` values that indicate passing
|
|
30
|
+
const PASSING_VERIFICATION_STATUSES = new Set([
|
|
31
|
+
'complete', 'verified', 'passed', 'human_passed',
|
|
32
|
+
]);
|
|
33
|
+
// VERIFICATION file frontmatter `status` values that explicitly block
|
|
34
|
+
const BLOCKING_VERIFICATION_FM_STATUSES = new Set([
|
|
35
|
+
'human_needed', 'gaps_found', 'pending', 'blocked', 'partial',
|
|
36
|
+
'failed', 'in_progress',
|
|
37
|
+
]);
|
|
38
|
+
// UAT test-item `result` values that count as passing
|
|
39
|
+
const PASSING_RESULTS = new Set(['passed', 'pass']);
|
|
40
|
+
// ─── stripFalsePositiveContexts ───────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Remove contexts that can contain `result: ...` lines that are NOT real test results:
|
|
43
|
+
* (a) leading frontmatter block at byte 0
|
|
44
|
+
* (b) HTML comments (unterminated comments swallow to EOF — fail-closed)
|
|
45
|
+
* (c) fenced code blocks (backtick and tilde, indented too) via CommonMark state machine
|
|
46
|
+
* (d) blockquote lines
|
|
47
|
+
*
|
|
48
|
+
* Each step is a composable function (Kernighan's Law — independently testable).
|
|
49
|
+
* Returns surviving lines joined by '\n'. Robust to CRLF input.
|
|
50
|
+
*/
|
|
51
|
+
function stripFalsePositiveContexts(content) {
|
|
52
|
+
// Step (a): strip leading frontmatter block only at byte 0
|
|
53
|
+
let stripped = content.replace(/^---\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/, '');
|
|
54
|
+
// Step (b): remove HTML comments anywhere; unterminated comment swallows to EOF
|
|
55
|
+
stripped = stripped.replace(/<!--[\s\S]*?(?:-->|$)/g, '');
|
|
56
|
+
// Step (c): remove fenced code blocks via CommonMark-style state machine (handles CRLF + indented fences)
|
|
57
|
+
stripped = _stripFencedBlocks(stripped).text;
|
|
58
|
+
// Step (d): remove blockquote lines
|
|
59
|
+
stripped = stripped
|
|
60
|
+
.split('\n')
|
|
61
|
+
.filter(line => !/^\s*>/.test(line))
|
|
62
|
+
.join('\n');
|
|
63
|
+
return stripped;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* CommonMark-style fenced-code-block stripper.
|
|
67
|
+
* Tracks the opening delimiter char and length so that a ~~~ line inside a
|
|
68
|
+
* ``` fence is correctly treated as fence content, not a closing delimiter.
|
|
69
|
+
*
|
|
70
|
+
* Opening rule: first delimiter line with char+len sets openFence.
|
|
71
|
+
* Closing rule: delimiter line with SAME char, run length >= openFence.len,
|
|
72
|
+
* and NO trailing non-whitespace text closes the fence.
|
|
73
|
+
* All delimiter and content lines are dropped; non-fence lines are kept.
|
|
74
|
+
* Returns the kept text plus unterminatedFence:true if EOF inside a fence.
|
|
75
|
+
*/
|
|
76
|
+
function _stripFencedBlocks(content) {
|
|
77
|
+
const lines = content.split('\n');
|
|
78
|
+
const kept = [];
|
|
79
|
+
let openFence = null;
|
|
80
|
+
const delimRe = /^(\s*)(`{3,}|~{3,})(.*)$/;
|
|
81
|
+
for (const rawLine of lines) {
|
|
82
|
+
// Tolerate CRLF: strip trailing \r for matching, but we work on split-by-\n lines
|
|
83
|
+
// (the outer caller joined by \n already; we just handle a stray \r in the last char)
|
|
84
|
+
const line = rawLine.replace(/\r$/, '');
|
|
85
|
+
const m = delimRe.exec(line);
|
|
86
|
+
if (m) {
|
|
87
|
+
const char = m[2][0];
|
|
88
|
+
const len = m[2].length;
|
|
89
|
+
const trailing = m[3];
|
|
90
|
+
if (openFence === null) {
|
|
91
|
+
// Opening delimiter — drop this line and record the fence
|
|
92
|
+
openFence = { char, len };
|
|
93
|
+
}
|
|
94
|
+
else if (char === openFence.char && len >= openFence.len && /^\s*$/.test(trailing)) {
|
|
95
|
+
// Closing delimiter (same char, sufficient length, no trailing text) — drop and close
|
|
96
|
+
openFence = null;
|
|
97
|
+
}
|
|
98
|
+
// else: mismatched delimiter inside fence (e.g. ~~~ inside ```) — drop as content
|
|
99
|
+
continue; // delimiter lines are always dropped
|
|
100
|
+
}
|
|
101
|
+
if (openFence === null) {
|
|
102
|
+
kept.push(rawLine);
|
|
103
|
+
}
|
|
104
|
+
// Lines inside fence are dropped
|
|
105
|
+
}
|
|
106
|
+
return { text: kept.join('\n'), unterminatedFence: openFence !== null };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Analyse raw markdown for structural anomalies (unterminated fence / comment).
|
|
110
|
+
* Exported for unit-testability and used by evaluateUatPassed for per-file malformed detection.
|
|
111
|
+
*
|
|
112
|
+
* FIX C: properly balanced comments are stripped before checking for a dangling <!--,
|
|
113
|
+
* so an earlier closed comment does not mask a later unterminated one.
|
|
114
|
+
*/
|
|
115
|
+
function analyzeMarkdown(raw) {
|
|
116
|
+
// Detect an unterminated HTML comment via a paired scan: every `<!--` must
|
|
117
|
+
// have a following `-->`. Using indexOf (not a regex .replace of the comment
|
|
118
|
+
// token) avoids the js/incomplete-multi-character-sanitization pattern — and
|
|
119
|
+
// is exact: a closed earlier comment never masks a later unterminated one.
|
|
120
|
+
let unterminatedComment = false;
|
|
121
|
+
for (let i = 0;;) {
|
|
122
|
+
const open = raw.indexOf('<!--', i);
|
|
123
|
+
if (open === -1)
|
|
124
|
+
break;
|
|
125
|
+
const close = raw.indexOf('-->', open + 4);
|
|
126
|
+
if (close === -1) {
|
|
127
|
+
unterminatedComment = true;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
i = close + 3;
|
|
131
|
+
}
|
|
132
|
+
// Fence state machine gives the accurate unterminated-fence signal.
|
|
133
|
+
const { unterminatedFence } = _stripFencedBlocks(raw);
|
|
134
|
+
return { unterminatedFence, unterminatedComment };
|
|
135
|
+
}
|
|
136
|
+
// ─── parseUatResultItems ──────────────────────────────────────────────────────
|
|
137
|
+
/**
|
|
138
|
+
* HEADING-BLOCK parser: scan the CLEANED body (after stripFalsePositiveContexts)
|
|
139
|
+
* for UAT test blocks.
|
|
140
|
+
*
|
|
141
|
+
* For each ### N. Name heading, the block spans until the next ### heading or EOF.
|
|
142
|
+
* Within each block, find a column-0 anchored result line (rejects indented YAML
|
|
143
|
+
* block-scalar bodies and inline/quoted fakes).
|
|
144
|
+
*
|
|
145
|
+
* - If a heading block has NO column-0 result line → emit result:'missing' (blocker).
|
|
146
|
+
* - Support bracketed [passed] and bare passed (#2273).
|
|
147
|
+
* - Returns ALL items (both passing and non-passing).
|
|
148
|
+
*/
|
|
149
|
+
function parseUatResultItems(cleanContent) {
|
|
150
|
+
const items = [];
|
|
151
|
+
// Find all ### N. Name headings (line-anchored)
|
|
152
|
+
const headingPattern = /^###\s*(\d+)\.\s*(.+)$/gm;
|
|
153
|
+
const headings = [];
|
|
154
|
+
let hMatch;
|
|
155
|
+
while ((hMatch = headingPattern.exec(cleanContent)) !== null) {
|
|
156
|
+
headings.push({
|
|
157
|
+
index: hMatch.index + hMatch[0].length,
|
|
158
|
+
test: parseInt(hMatch[1], 10),
|
|
159
|
+
name: hMatch[2].trim(),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
for (let i = 0; i < headings.length; i++) {
|
|
163
|
+
const h = headings[i];
|
|
164
|
+
const blockStart = h.index;
|
|
165
|
+
// More precise: find next heading's position in the original string
|
|
166
|
+
// We'll slice from current heading end to the position just before next heading's "###"
|
|
167
|
+
const nextHeadingMatch = i + 1 < headings.length
|
|
168
|
+
? cleanContent.lastIndexOf('\n###', headings[i + 1].index)
|
|
169
|
+
: -1;
|
|
170
|
+
const blockContent = nextHeadingMatch >= blockStart
|
|
171
|
+
? cleanContent.slice(blockStart, nextHeadingMatch)
|
|
172
|
+
: cleanContent.slice(blockStart);
|
|
173
|
+
// Column-0 anchored result line: /^result:[ \t]*\[?([\w-]+)\]?/mi
|
|
174
|
+
// Uses [ \t]* (not \s*) so the captured value must sit on the SAME line as result:.
|
|
175
|
+
// A result: key with the value on a subsequent line yields no match → 'missing' (blocker).
|
|
176
|
+
const resultMatch = /^result:[ \t]*\[?([\w-]+)\]?/mi.exec(blockContent);
|
|
177
|
+
if (resultMatch) {
|
|
178
|
+
items.push({
|
|
179
|
+
test: h.test,
|
|
180
|
+
name: h.name,
|
|
181
|
+
result: resultMatch[1].toLowerCase(),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// No column-0 result line → emit 'missing' (a non-passing state)
|
|
186
|
+
items.push({
|
|
187
|
+
test: h.test,
|
|
188
|
+
name: h.name,
|
|
189
|
+
result: 'missing',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return items;
|
|
194
|
+
}
|
|
195
|
+
// ─── evaluateUatPassed ────────────────────────────────────────────────────────
|
|
196
|
+
/**
|
|
197
|
+
* Evaluate all UAT/VERIFICATION files in a phase directory.
|
|
198
|
+
* Returns a UatPassedReport with the locked, stable shape defined by the interface.
|
|
199
|
+
*
|
|
200
|
+
* FAIL-CLOSED: any absence/ambiguity/malformed input → NOT passed.
|
|
201
|
+
* Pass requires at least one real passing check AND no blockers.
|
|
202
|
+
*/
|
|
203
|
+
function evaluateUatPassed(phaseFullDir, opts) {
|
|
204
|
+
const requireVerification = opts?.policy?.requireVerification === true;
|
|
205
|
+
const blockers = [];
|
|
206
|
+
const checks = [];
|
|
207
|
+
const uatFiles = [];
|
|
208
|
+
const verificationFiles = [];
|
|
209
|
+
// Read the directory — if unreadable, treat as no files (fail-closed: no artifacts → not passed)
|
|
210
|
+
let dirEntries = [];
|
|
211
|
+
try {
|
|
212
|
+
dirEntries = node_fs_1.default.readdirSync(phaseFullDir);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Unreadable dir — no_uat_artifacts:true, passed:false
|
|
216
|
+
const no_uat_artifacts = true;
|
|
217
|
+
if (requireVerification) {
|
|
218
|
+
blockers.push('policy: verification required but no passing *-VERIFICATION.md found');
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
passed: false,
|
|
222
|
+
uat_files: [],
|
|
223
|
+
verification_files: [],
|
|
224
|
+
checks: [],
|
|
225
|
+
blockers,
|
|
226
|
+
no_uat_artifacts,
|
|
227
|
+
policy: { require_verification: requireVerification },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Filter UAT and VERIFICATION files using the same filter as cmdPhaseComplete
|
|
231
|
+
const uatFileNames = dirEntries.filter(f => f.includes('-UAT') && f.endsWith('.md'));
|
|
232
|
+
const verFileNames = dirEntries.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'));
|
|
233
|
+
// ── Process UAT files ──────────────────────────────────────────────────────
|
|
234
|
+
for (const file of uatFileNames) {
|
|
235
|
+
uatFiles.push(file);
|
|
236
|
+
let raw = '';
|
|
237
|
+
try {
|
|
238
|
+
raw = node_fs_1.default.readFileSync(node_path_1.default.join(phaseFullDir, file), 'utf-8');
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
blockers.push(`${file}: could not read file`);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
// ── Per-file malformed markdown guard ──────────────────────────────────
|
|
245
|
+
// FIX D: use accurate signals from analyzeMarkdown instead of heuristics.
|
|
246
|
+
// unterminatedFence: CommonMark state machine detects a genuinely unclosed fence.
|
|
247
|
+
// unterminatedComment: strips balanced comments first, then checks for leftover <!--.
|
|
248
|
+
const { unterminatedFence, unterminatedComment } = analyzeMarkdown(raw);
|
|
249
|
+
if (unterminatedFence || unterminatedComment) {
|
|
250
|
+
blockers.push(`${file}: malformed markdown (unterminated fence or comment)`);
|
|
251
|
+
}
|
|
252
|
+
const fm = extractFrontmatter(raw);
|
|
253
|
+
// File-level frontmatter status check
|
|
254
|
+
if (fm['status'] && BLOCKING_UAT_FM_STATUSES.has(fm['status'])) {
|
|
255
|
+
blockers.push(`${file}: frontmatter status=${fm['status']}`);
|
|
256
|
+
}
|
|
257
|
+
// File-level frontmatter result check
|
|
258
|
+
if (fm['result'] && BLOCKING_UAT_FM_RESULTS.has(fm['result'])) {
|
|
259
|
+
blockers.push(`${file}: frontmatter result=${fm['result']}`);
|
|
260
|
+
}
|
|
261
|
+
// Parse test items from the cleaned body (hardened against false positives)
|
|
262
|
+
const cleanContent = stripFalsePositiveContexts(raw);
|
|
263
|
+
const items = parseUatResultItems(cleanContent);
|
|
264
|
+
for (const item of items) {
|
|
265
|
+
const passing = PASSING_RESULTS.has(item.result);
|
|
266
|
+
checks.push({
|
|
267
|
+
file,
|
|
268
|
+
test: item.test,
|
|
269
|
+
name: item.name,
|
|
270
|
+
result: item.result,
|
|
271
|
+
passing,
|
|
272
|
+
});
|
|
273
|
+
if (!passing) {
|
|
274
|
+
blockers.push(`${file}: test ${item.test} (${item.result})`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// ── Process VERIFICATION files ─────────────────────────────────────────────
|
|
279
|
+
let hasPassingVerification = false;
|
|
280
|
+
for (const file of verFileNames) {
|
|
281
|
+
verificationFiles.push(file);
|
|
282
|
+
let raw = '';
|
|
283
|
+
try {
|
|
284
|
+
raw = node_fs_1.default.readFileSync(node_path_1.default.join(phaseFullDir, file), 'utf-8');
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
blockers.push(`${file}: could not read verification file`);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const vfm = extractFrontmatter(raw);
|
|
291
|
+
const vStatus = vfm['status'];
|
|
292
|
+
if (vStatus && BLOCKING_VERIFICATION_FM_STATUSES.has(vStatus)) {
|
|
293
|
+
blockers.push(`${file}: verification status=${vStatus}`);
|
|
294
|
+
}
|
|
295
|
+
else if (vStatus && PASSING_VERIFICATION_STATUSES.has(vStatus)) {
|
|
296
|
+
// Allowlist: only explicitly-passing statuses count
|
|
297
|
+
hasPassingVerification = true;
|
|
298
|
+
}
|
|
299
|
+
// Missing or unknown status: does NOT count as passing, does NOT push a blocker
|
|
300
|
+
// (handled by the requireVerification policy check below if needed)
|
|
301
|
+
}
|
|
302
|
+
// ── Policy: requireVerification ───────────────────────────────────────────
|
|
303
|
+
if (requireVerification && !hasPassingVerification) {
|
|
304
|
+
blockers.push('policy: verification required but no passing *-VERIFICATION.md found');
|
|
305
|
+
}
|
|
306
|
+
// ── Determine no_uat_artifacts and passed ─────────────────────────────────
|
|
307
|
+
// no_uat_artifacts: true when no real UAT test items were parsed from any file
|
|
308
|
+
const no_uat_artifacts = checks.length === 0;
|
|
309
|
+
// FIX 1: require positive passing evidence; no vacuous pass
|
|
310
|
+
// passed = no blockers AND at least one check AND all checks passing
|
|
311
|
+
const passed = blockers.length === 0 && checks.length > 0 && checks.every(c => c.passing);
|
|
312
|
+
return {
|
|
313
|
+
passed,
|
|
314
|
+
uat_files: uatFiles,
|
|
315
|
+
verification_files: verificationFiles,
|
|
316
|
+
checks,
|
|
317
|
+
blockers,
|
|
318
|
+
no_uat_artifacts,
|
|
319
|
+
policy: {
|
|
320
|
+
require_verification: requireVerification,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
module.exports = {
|
|
325
|
+
stripFalsePositiveContexts,
|
|
326
|
+
parseUatResultItems,
|
|
327
|
+
analyzeMarkdown,
|
|
328
|
+
evaluateUatPassed,
|
|
329
|
+
};
|
|
@@ -29,7 +29,10 @@ exports.RUNTIME_DIRS = [
|
|
|
29
29
|
['antigravity', '.gemini/antigravity-ide'],
|
|
30
30
|
['antigravity', '.gemini/antigravity-cli'],
|
|
31
31
|
['antigravity', '.gemini/antigravity'],
|
|
32
|
-
['antigravity', '.
|
|
32
|
+
['antigravity', '.agents'], // local Antigravity install dir canonical (#791; bin/install.js getDirName('antigravity'))
|
|
33
|
+
['antigravity', '.agent'], // local Antigravity install dir legacy (#503; backward-compat with pre-#791 installs)
|
|
34
|
+
['windsurf', '.devin'], // local Windsurf/Devin Desktop install dir canonical (#1085; bin/install.js getDirName('windsurf'))
|
|
35
|
+
['windsurf', '.windsurf'], // local Windsurf install dir legacy (#1085; backward-compat with pre-#1085 installs)
|
|
33
36
|
['gemini', '.gemini'],
|
|
34
37
|
['kilo', '.config/kilo'],
|
|
35
38
|
['kilo', '.kilo'],
|
|
@@ -121,6 +121,98 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
|
|
|
121
121
|
const result = { passed, checks, errors };
|
|
122
122
|
output(result, raw, passed ? 'passed' : 'failed');
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Issue #429 — negative-grep comment-text echo gate.
|
|
126
|
+
* A literal that an acceptance criterion negative-greps for (grep -c 'LIT' file == 0)
|
|
127
|
+
* must not also appear verbatim inside an <action> body, or the executor's commit-time
|
|
128
|
+
* verify gate fails on the comment echo rather than a real regression. Conservative:
|
|
129
|
+
* errors only on a confidently-extracted QUOTED literal; ambiguous (bareword) → warning.
|
|
130
|
+
*/
|
|
131
|
+
function scanNegativeGrepCommentEcho(content) {
|
|
132
|
+
const errors = [];
|
|
133
|
+
const warnings = [];
|
|
134
|
+
// Normalize newlines; join backslash line-continuations so a verify command wrapped
|
|
135
|
+
// across lines (grep ... \ <newline> == 0) is still seen as one segment.
|
|
136
|
+
const text = (content || '')
|
|
137
|
+
.replace(/\r\n/g, '\n')
|
|
138
|
+
.replace(/\r/g, '\n')
|
|
139
|
+
.replace(/\\\n/g, ' ');
|
|
140
|
+
// 1. Allowlisted literals: <!-- planner-discipline-allow: LIT -->
|
|
141
|
+
const allow = new Set();
|
|
142
|
+
const allowRe = /<!--\s*planner-discipline-allow:\s*(.+?)\s*-->/g;
|
|
143
|
+
let am;
|
|
144
|
+
while ((am = allowRe.exec(text)) !== null)
|
|
145
|
+
allow.add(am[1]);
|
|
146
|
+
// Zero-equality comparison (the negative grep). The required leading whitespace
|
|
147
|
+
// before the operator distinguishes a shell comparison (`[ $c == 0 ]`, `... == 0`,
|
|
148
|
+
// always spaced) from an assignment (`VAR=0`, never spaced) and naturally excludes
|
|
149
|
+
// `>= 0`, `<= 0`, `!= 0`, `!== 0`, `=== 0`.
|
|
150
|
+
const zeroCmp = (s) => /\s==?\s*0\b/.test(s) || /-eq\s+0\b/.test(s) || /\bequals\s+0\b/.test(s);
|
|
151
|
+
// A grep invocation using a count flag (-c / -cF / -Fc / --count), capturing the
|
|
152
|
+
// search pattern (first quoted token, else first bareword) after a run of options.
|
|
153
|
+
// The options run lets `grep -c -F 'LIT'`, `grep -F -c 'LIT'`, `grep -c -e 'LIT'`
|
|
154
|
+
// and `grep --count 'LIT'` all resolve to the LIT pattern.
|
|
155
|
+
const countGrepRe = /grep((?:\s+-{1,2}[A-Za-z][A-Za-z-]*)+)\s+(?:'([^']*)'|"([^"]*)"|([^\s'"|>&;]+))/g;
|
|
156
|
+
const optsHaveCount = (opts) => /(?:^|\s)-[A-Za-z]*c[A-Za-z]*(?=\s|$)/.test(opts) || /--count\b/.test(opts);
|
|
157
|
+
// `grep -cv 'pat' == 0` counts NON-matching lines, so == 0 there asserts "all lines
|
|
158
|
+
// match" — a POSITIVE gate, not our negative gate. Skip inverted greps.
|
|
159
|
+
const optsHaveInvert = (opts) => /(?:^|\s)-[A-Za-z]*v[A-Za-z]*(?=\s|$)/.test(opts) || /--invert-match\b/.test(opts);
|
|
160
|
+
// Bareword sanity: a real grep target, not a stray operator/number/flag.
|
|
161
|
+
const plausibleBare = (s) => /[A-Za-z0-9_]/.test(s) && !/^[-=!<>0-9]+$/.test(s);
|
|
162
|
+
// 2. <action> text to scan, with negative-grep COMMAND SPANS removed (only the
|
|
163
|
+
// command, not the whole line) so a pasted verify command does not self-flag
|
|
164
|
+
// while a prose echo on the same line is still caught.
|
|
165
|
+
const cmdSpanRe = /grep(?:\s+-{1,2}[A-Za-z][A-Za-z-]*)+\s+(?:'[^']*'|"[^"]*"|[^\s'"|>&;]+)[^\n]*?(?:==|-eq|=)\s*0\b/g;
|
|
166
|
+
const actionZones = [];
|
|
167
|
+
const actionRe = /<action>([\s\S]*?)<\/action>/g;
|
|
168
|
+
let acm;
|
|
169
|
+
while ((acm = actionRe.exec(text)) !== null)
|
|
170
|
+
actionZones.push(acm[1]);
|
|
171
|
+
const scannableActionText = actionZones.map((zone) => zone.replace(cmdSpanRe, ' ')).join('\n');
|
|
172
|
+
// 3. Per shell SEGMENT (split lines on && / ||) extract count-grep literals and
|
|
173
|
+
// check echoes. Per-segment splitting keeps a positive gate (`== 1`) from
|
|
174
|
+
// poisoning a negative gate (`== 0`) sharing the same physical line.
|
|
175
|
+
const seenErr = new Set();
|
|
176
|
+
const seenWarn = new Set();
|
|
177
|
+
const segments = text.split('\n').flatMap((line) => line.split(/\s*(?:&&|\|\|)\s*/));
|
|
178
|
+
for (const seg of segments) {
|
|
179
|
+
if (!/grep(?:\s+-{1,2}[A-Za-z])/.test(seg) || !zeroCmp(seg))
|
|
180
|
+
continue;
|
|
181
|
+
countGrepRe.lastIndex = 0;
|
|
182
|
+
const quotedLits = [];
|
|
183
|
+
const bareLits = [];
|
|
184
|
+
let m;
|
|
185
|
+
while ((m = countGrepRe.exec(seg)) !== null) {
|
|
186
|
+
if (!optsHaveCount(m[1]) || optsHaveInvert(m[1]))
|
|
187
|
+
continue; // need count, not invert (-cv is positive)
|
|
188
|
+
if (m[2] !== undefined)
|
|
189
|
+
quotedLits.push(m[2]);
|
|
190
|
+
else if (m[3] !== undefined)
|
|
191
|
+
quotedLits.push(m[3]);
|
|
192
|
+
else if (m[4] !== undefined && plausibleBare(m[4]))
|
|
193
|
+
bareLits.push(m[4]);
|
|
194
|
+
}
|
|
195
|
+
for (const quoted of quotedLits) {
|
|
196
|
+
if (!quoted || allow.has(quoted) || seenErr.has(quoted))
|
|
197
|
+
continue;
|
|
198
|
+
if (scannableActionText.includes(quoted)) {
|
|
199
|
+
seenErr.add(quoted);
|
|
200
|
+
errors.push(`Plan body contains forbidden literal "${quoted}" in an <action> block, but an acceptance criterion negative-greps for it (grep -c ... == 0). Rephrase the literal by concept, remove it from the plan body, or add <!-- planner-discipline-allow: ${quoted} --> if it must legitimately appear.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (quotedLits.length === 0) {
|
|
204
|
+
for (const bare of bareLits) {
|
|
205
|
+
if (allow.has(bare) || seenWarn.has(bare))
|
|
206
|
+
continue;
|
|
207
|
+
if (scannableActionText.includes(bare)) {
|
|
208
|
+
seenWarn.add(bare);
|
|
209
|
+
warnings.push(`Possible comment-text echo (#429): negative-grep target "${bare}" is unquoted so its literal could not be extracted unambiguously, but it appears in an <action> block. Quote the grep literal and add an allowlist marker if the echo is intended, or rephrase by concept.`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { errors, warnings };
|
|
215
|
+
}
|
|
124
216
|
function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
125
217
|
if (!filePath) {
|
|
126
218
|
error('file path required');
|
|
@@ -175,6 +267,9 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
175
267
|
if (hasCheckpoints && fm['autonomous'] !== 'false' && String(fm['autonomous']) !== 'false') {
|
|
176
268
|
errors.push('Has checkpoint tasks but autonomous is not false');
|
|
177
269
|
}
|
|
270
|
+
const echoScan = scanNegativeGrepCommentEcho(content);
|
|
271
|
+
errors.push(...echoScan.errors);
|
|
272
|
+
warnings.push(...echoScan.warnings);
|
|
178
273
|
output({
|
|
179
274
|
valid: errors.length === 0,
|
|
180
275
|
errors,
|
|
@@ -383,7 +478,7 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
383
478
|
};
|
|
384
479
|
const sourceContent = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(cwd, link['from'] || ''));
|
|
385
480
|
if (!sourceContent) {
|
|
386
|
-
check['detail'] = 'Source file not found';
|
|
481
|
+
check['detail'] = 'Source file not found (from: must be a relative file path; describe components/endpoints in via:)';
|
|
387
482
|
}
|
|
388
483
|
else if (link['pattern']) {
|
|
389
484
|
try {
|
|
@@ -826,6 +921,12 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
826
921
|
if ((agentStatus.installed_agents).length === 0) {
|
|
827
922
|
addIssue('warning', 'W010', `No GSD agents found in ${agentStatus.agents_dir} — Task(subagent_type="gsd-*") will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
|
|
828
923
|
}
|
|
924
|
+
else if ((agentStatus.incomplete_agents).length > 0 && (agentStatus.missing_agents).length === 0) {
|
|
925
|
+
addIssue('warning', 'W010', `Incomplete agent installs (missing generated file): ${(agentStatus.incomplete_agents).join(', ')} — affected workflows may fall back to general-purpose`, `Re-run the GSD installer to complete the install: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
|
|
926
|
+
}
|
|
927
|
+
else if ((agentStatus.incomplete_agents).length > 0) {
|
|
928
|
+
addIssue('warning', 'W010', `Missing ${(agentStatus.missing_agents).length} GSD agents: ${(agentStatus.missing_agents).join(', ')}; incomplete agent installs (missing generated file): ${(agentStatus.incomplete_agents).join(', ')} — affected workflows will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
|
|
929
|
+
}
|
|
829
930
|
else {
|
|
830
931
|
addIssue('warning', 'W010', `Missing ${(agentStatus.missing_agents).length} GSD agents: ${(agentStatus.missing_agents).join(', ')} — affected workflows will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
|
|
831
932
|
}
|
|
@@ -1239,6 +1340,7 @@ function cmdValidateAgents(cwd, raw) {
|
|
|
1239
1340
|
agents_found: agentStatus.agents_installed,
|
|
1240
1341
|
installed: agentStatus.installed_agents,
|
|
1241
1342
|
missing: agentStatus.missing_agents,
|
|
1343
|
+
incomplete: agentStatus.incomplete_agents,
|
|
1242
1344
|
expected,
|
|
1243
1345
|
}, raw);
|
|
1244
1346
|
}
|
|
@@ -1423,6 +1525,7 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
|
|
|
1423
1525
|
}
|
|
1424
1526
|
}
|
|
1425
1527
|
module.exports = {
|
|
1528
|
+
scanNegativeGrepCommentEcho,
|
|
1426
1529
|
cmdVerifySummary,
|
|
1427
1530
|
cmdVerifyPlanStructure,
|
|
1428
1531
|
cmdVerifyPhaseCompleteness,
|
|
@@ -24,6 +24,7 @@ exports.evaluateWorktreeBaseDegrade = evaluateWorktreeBaseDegrade;
|
|
|
24
24
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
25
25
|
const node_path_1 = __importDefault(require("node:path"));
|
|
26
26
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
27
|
+
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
27
28
|
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
28
29
|
/**
|
|
29
30
|
* Strip JSONC comments (line and block forms) from a string to produce valid JSON.
|
|
@@ -153,12 +154,18 @@ function applyWorktreeBaseRef(settings) {
|
|
|
153
154
|
return { changed: true, settings, skipped: null, previous: null };
|
|
154
155
|
}
|
|
155
156
|
/**
|
|
156
|
-
* Reads settings
|
|
157
|
-
*
|
|
157
|
+
* Reads settings files in a 3-layer cascade and extracts worktree.baseRef from
|
|
158
|
+
* the first layer that provides a non-null string value. Layers (highest to lowest
|
|
159
|
+
* precedence):
|
|
160
|
+
* 1. project local — <claudeDir>/settings.local.json
|
|
161
|
+
* 2. project shared — <claudeDir>/settings.json
|
|
162
|
+
* 3. user/global — <userClaudeDir>/settings.json (only when userClaudeDir is
|
|
163
|
+
* provided AND resolves to a different path than claudeDir)
|
|
158
164
|
*
|
|
159
165
|
* deps.readFile(path) must return the file contents or null on any error.
|
|
166
|
+
* userClaudeDir is optional; when absent/null the user/global layer is skipped.
|
|
160
167
|
*/
|
|
161
|
-
function resolveEffectiveBaseRef(claudeDir, deps) {
|
|
168
|
+
function resolveEffectiveBaseRef(claudeDir, deps, userClaudeDir) {
|
|
162
169
|
const readFile = deps?.readFile ?? ((p) => {
|
|
163
170
|
try {
|
|
164
171
|
return node_fs_1.default.readFileSync(p, 'utf8');
|
|
@@ -181,21 +188,39 @@ function resolveEffectiveBaseRef(claudeDir, deps) {
|
|
|
181
188
|
return null;
|
|
182
189
|
}
|
|
183
190
|
}
|
|
191
|
+
// Layer 1: project local
|
|
184
192
|
const localRef = parseBaseRef(localPath);
|
|
185
193
|
if (localRef !== null)
|
|
186
194
|
return localRef;
|
|
187
|
-
|
|
195
|
+
// Layer 2: project shared
|
|
196
|
+
const sharedRef = parseBaseRef(sharedPath);
|
|
197
|
+
if (sharedRef !== null)
|
|
198
|
+
return sharedRef;
|
|
199
|
+
// Layer 3: user/global (only when provided and not the same directory as claudeDir)
|
|
200
|
+
if (userClaudeDir && node_path_1.default.resolve(userClaudeDir) !== node_path_1.default.resolve(claudeDir)) {
|
|
201
|
+
const userSharedPath = node_path_1.default.join(userClaudeDir, 'settings.json');
|
|
202
|
+
const userRef = parseBaseRef(userSharedPath);
|
|
203
|
+
if (userRef !== null)
|
|
204
|
+
return userRef;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
188
207
|
}
|
|
189
208
|
/**
|
|
190
209
|
* CLI command: check current worktree base-ref degradation status.
|
|
191
210
|
*
|
|
192
|
-
* Reads effective baseRef from <cwd>/.claude settings
|
|
193
|
-
*
|
|
194
|
-
* the result object.
|
|
211
|
+
* Reads effective baseRef from <cwd>/.claude settings (3-layer cascade:
|
|
212
|
+
* project local → project shared → user/global), runs degradation evaluation,
|
|
213
|
+
* writes JSON result to stdout (or injected write), and returns the result object.
|
|
214
|
+
*
|
|
215
|
+
* deps.userClaudeDir overrides the user/global config directory resolution
|
|
216
|
+
* (default: getGlobalConfigDir('claude'), which honours CLAUDE_CONFIG_DIR).
|
|
195
217
|
*/
|
|
196
218
|
function cmdWorktreeBaseCheck(cwd, _args, deps) {
|
|
197
219
|
const claudeDir = node_path_1.default.join(cwd, '.claude');
|
|
198
|
-
const
|
|
220
|
+
const userClaudeDir = Object.prototype.hasOwnProperty.call(deps ?? {}, 'userClaudeDir')
|
|
221
|
+
? deps.userClaudeDir
|
|
222
|
+
: (0, runtime_homes_cjs_1.getGlobalConfigDir)('claude');
|
|
223
|
+
const effectiveBaseRef = resolveEffectiveBaseRef(claudeDir, deps?.readFile ? { readFile: deps.readFile } : undefined, userClaudeDir);
|
|
199
224
|
const result = evaluateWorktreeBaseDegrade({
|
|
200
225
|
cwd,
|
|
201
226
|
effectiveBaseRef,
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
},
|
|
15
15
|
"codex": {
|
|
16
16
|
"opus": { "model": "gpt-5.5", "reasoning_effort": "xhigh" },
|
|
17
|
-
"sonnet": { "model": "gpt-5.
|
|
17
|
+
"sonnet": { "model": "gpt-5.4", "reasoning_effort": "medium" },
|
|
18
18
|
"haiku": { "model": "gpt-5.4-mini", "reasoning_effort": "medium" }
|
|
19
19
|
},
|
|
20
20
|
"gemini": {
|
|
21
|
-
"opus": { "model": "gemini-3-pro" },
|
|
21
|
+
"opus": { "model": "gemini-3.1-pro-preview" },
|
|
22
22
|
"sonnet": { "model": "gemini-3-flash" },
|
|
23
23
|
"haiku": { "model": "gemini-2.5-flash-lite" }
|
|
24
24
|
},
|
|
@@ -52,6 +52,11 @@
|
|
|
52
52
|
"sonnet": null,
|
|
53
53
|
"haiku": null
|
|
54
54
|
},
|
|
55
|
+
"kimi": {
|
|
56
|
+
"opus": null,
|
|
57
|
+
"sonnet": null,
|
|
58
|
+
"haiku": null
|
|
59
|
+
},
|
|
55
60
|
"cursor": {
|
|
56
61
|
"opus": null,
|
|
57
62
|
"sonnet": null,
|
|
@@ -95,12 +100,12 @@
|
|
|
95
100
|
"haiku": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-haiku-4-5" }, "high": { "model": "claude-sonnet-4-6" } }
|
|
96
101
|
},
|
|
97
102
|
"openai": {
|
|
98
|
-
"opus": { "low": { "model": "gpt-5.
|
|
99
|
-
"sonnet": { "low": { "model": "gpt-5.4-mini", "reasoning_effort": "low" }, "medium": { "model": "gpt-5.
|
|
100
|
-
"haiku": { "low": { "model": "gpt-5.4-mini", "reasoning_effort": "minimal" }, "medium": { "model": "gpt-5.4-mini", "reasoning_effort": "medium" }, "high": { "model": "gpt-5.
|
|
103
|
+
"opus": { "low": { "model": "gpt-5.4", "reasoning_effort": "medium" }, "medium": { "model": "gpt-5.5", "reasoning_effort": "high" }, "high": { "model": "gpt-5.5", "reasoning_effort": "xhigh" } },
|
|
104
|
+
"sonnet": { "low": { "model": "gpt-5.4-mini", "reasoning_effort": "low" }, "medium": { "model": "gpt-5.4", "reasoning_effort": "medium" }, "high": { "model": "gpt-5.5", "reasoning_effort": "medium" } },
|
|
105
|
+
"haiku": { "low": { "model": "gpt-5.4-mini", "reasoning_effort": "minimal" }, "medium": { "model": "gpt-5.4-mini", "reasoning_effort": "medium" }, "high": { "model": "gpt-5.4", "reasoning_effort": "medium" } }
|
|
101
106
|
},
|
|
102
107
|
"google": {
|
|
103
|
-
"opus": { "low": { "model": "gemini-2.5-flash-lite" }, "medium": { "model": "gemini-3-flash" }, "high": { "model": "gemini-3-pro" } },
|
|
108
|
+
"opus": { "low": { "model": "gemini-2.5-flash-lite" }, "medium": { "model": "gemini-3-flash" }, "high": { "model": "gemini-3.1-pro-preview" } },
|
|
104
109
|
"sonnet": { "low": { "model": "gemini-2.5-flash-lite" }, "medium": { "model": "gemini-3-flash" }, "high": { "model": "gemini-3-flash" } },
|
|
105
110
|
"haiku": { "low": { "model": "gemini-2.5-flash-lite" }, "medium": { "model": "gemini-2.5-flash-lite" }, "high": { "model": "gemini-3-flash" } }
|
|
106
111
|
},
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"windsurf": [
|
|
44
44
|
"windsurf",
|
|
45
45
|
"windsurf-cli",
|
|
46
|
-
"windsurf-next"
|
|
46
|
+
"windsurf-next",
|
|
47
|
+
"devin-desktop"
|
|
47
48
|
],
|
|
48
49
|
"augment": [
|
|
49
50
|
"augment",
|
|
@@ -64,6 +65,9 @@
|
|
|
64
65
|
"hermes-agent",
|
|
65
66
|
"hermes-cli"
|
|
66
67
|
],
|
|
68
|
+
"kimi": [
|
|
69
|
+
"kimi"
|
|
70
|
+
],
|
|
67
71
|
"codebuddy": [
|
|
68
72
|
"codebuddy",
|
|
69
73
|
"codebuddy-cli"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"items": [
|
|
3
|
+
{ "requirement_id": "R1", "category": "boundary", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What happens exactly at each min/max/threshold — and one step either side?" },
|
|
4
|
+
{ "requirement_id": "R1", "category": "precision", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "Where can precision loss, overflow, or rounding/tie-breaking occur — and what is the exact contract (e.g. half-up vs half-to-even, ceil/floor/truncate)?" }
|
|
5
|
+
],
|
|
6
|
+
"coverage": { "applicable": 2, "resolved": 0, "unresolved": 2, "byVerification": { "explicit": 0, "backstop": 0 } }
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{ "id": "R1", "text": "Round a number to N decimal places, rounding half to nearest" }]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"items": [
|
|
3
|
+
{ "requirement_id": "R1", "category": "adjacency", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?" },
|
|
4
|
+
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
|
|
5
|
+
{ "requirement_id": "R1", "category": "ordering", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When elements compare equal, is output order specified and stable?" }
|
|
6
|
+
],
|
|
7
|
+
"coverage": { "applicable": 3, "resolved": 0, "unresolved": 3, "byVerification": { "explicit": 0, "backstop": 0 } }
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{ "id": "R1", "text": "Merge a list of overlapping intervals into the minimal set" }]
|