@opengsd/gsd-core 1.16.0 → 1.18.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 -2
- package/.opencode/plugins/gsd-core.js +251 -1
- package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
- package/GEMINI.md +1 -1
- package/agents/gsd-advisor-researcher.compact.md +1 -1
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.compact.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.compact.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.compact.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.compact.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.compact.md +6 -0
- package/agents/gsd-debug-session-manager.md +6 -0
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-verifier.compact.md +4 -4
- package/agents/gsd-doc-verifier.md +4 -3
- package/agents/gsd-doc-writer.compact.md +23 -5
- package/agents/gsd-doc-writer.md +19 -6
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +7 -3
- package/agents/gsd-integration-checker.compact.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-nyquist-auditor.compact.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -1
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +2 -1
- package/agents/gsd-planner.md +5 -4
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +5 -5
- package/agents/gsd-research-synthesizer.md +5 -5
- package/agents/gsd-roadmapper.compact.md +1 -1
- package/agents/gsd-roadmapper.md +1 -1
- package/agents/gsd-security-auditor.compact.md +1 -1
- package/agents/gsd-security-auditor.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +1 -1
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.compact.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.compact.md +13 -1
- package/agents/gsd-ui-researcher.md +28 -1
- package/agents/gsd-verifier.md +1 -1
- package/bin/gsd-mcp-server.js +3 -1
- package/bin/install.js +398 -95
- package/commands/gsd/code-review.md +8 -0
- package/commands/gsd/pause-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +34 -12
- package/gsd-core/bin/lib/adr-parser.cjs +37 -5
- package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
- package/gsd-core/bin/lib/capability-lock.cjs +29 -7
- package/gsd-core/bin/lib/capability-registry.cjs +138 -80
- package/gsd-core/bin/lib/capability-source.cjs +3 -1
- package/gsd-core/bin/lib/capability-state.cjs +6 -3
- package/gsd-core/bin/lib/capability-validator.cjs +8 -5
- package/gsd-core/bin/lib/capability-writer.cjs +2 -1
- package/gsd-core/bin/lib/commands.cjs +33 -9
- package/gsd-core/bin/lib/config-loader.cjs +3 -3
- package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
- package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
- package/gsd-core/bin/lib/gate-ui-safety.cjs +2 -1
- package/gsd-core/bin/lib/graphify.cjs +3 -2
- package/gsd-core/bin/lib/init.cjs +43 -16
- package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
- package/gsd-core/bin/lib/install-engine.cjs +11 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
- package/gsd-core/bin/lib/install-profiles.cjs +5 -3
- package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
- package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
- package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
- package/gsd-core/bin/lib/mcp-server.cjs +5 -1
- package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
- package/gsd-core/bin/lib/opencode-version.cjs +486 -0
- package/gsd-core/bin/lib/phase.cjs +141 -11
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
- package/gsd-core/bin/lib/report-parser.cjs +57 -1
- package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
- package/gsd-core/bin/lib/roadmap.cjs +56 -5
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +142 -6
- package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +58 -3
- package/gsd-core/bin/lib/runtime-slash.cjs +13 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
- package/gsd-core/bin/lib/smart-entry.cjs +100 -53
- package/gsd-core/bin/lib/state-document.cjs +37 -3
- package/gsd-core/bin/lib/state-transition.cjs +98 -19
- package/gsd-core/bin/lib/state.cjs +544 -126
- package/gsd-core/bin/lib/task-command-router.cjs +2 -3
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
- package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
- package/gsd-core/bin/lib/update-context.cjs +18 -1
- package/gsd-core/bin/lib/verification.cjs +107 -17
- package/gsd-core/bin/lib/verify.cjs +36 -1
- package/gsd-core/references/agent-contracts.md +1 -1
- package/gsd-core/references/agent-skills-bootstrap.md +26 -7
- package/gsd-core/references/git-integration.md +3 -3
- package/gsd-core/references/planner-reversibility.md +18 -4
- package/gsd-core/references/planner-verify-command-grounding.md +18 -0
- package/gsd-core/references/tdd.md +4 -3
- package/gsd-core/references/verifier-phase-gates.md +8 -1
- package/gsd-core/workflows/add-backlog.md +10 -2
- package/gsd-core/workflows/add-phase.md +5 -1
- package/gsd-core/workflows/add-tests.md +5 -1
- package/gsd-core/workflows/add-todo.md +5 -1
- package/gsd-core/workflows/ai-integration-phase.md +5 -1
- package/gsd-core/workflows/audit-milestone.md +21 -9
- package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
- package/gsd-core/workflows/autonomous.md +52 -64
- package/gsd-core/workflows/check-todos.md +5 -1
- package/gsd-core/workflows/code-review-fix.md +5 -1
- package/gsd-core/workflows/code-review.md +21 -2
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
- package/gsd-core/workflows/complete-milestone.md +25 -5
- package/gsd-core/workflows/debug.md +5 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
- package/gsd-core/workflows/discuss-phase.md +5 -1
- package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
- package/gsd-core/workflows/docs-update.md +27 -5
- package/gsd-core/workflows/edit-phase.md +5 -1
- package/gsd-core/workflows/eval-review.md +11 -3
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
- package/gsd-core/workflows/execute-phase.md +40 -7
- package/gsd-core/workflows/execute-plan.md +8 -4
- package/gsd-core/workflows/extract-learnings.md +6 -2
- package/gsd-core/workflows/forensics.md +5 -1
- package/gsd-core/workflows/import.md +5 -1
- package/gsd-core/workflows/insert-phase.md +5 -1
- package/gsd-core/workflows/manager.md +10 -2
- package/gsd-core/workflows/map-codebase.md +5 -1
- package/gsd-core/workflows/milestone-summary.md +10 -2
- package/gsd-core/workflows/new-milestone.md +13 -5
- package/gsd-core/workflows/new-project.md +9 -5
- package/gsd-core/workflows/next.md +8 -4
- package/gsd-core/workflows/pause-work.md +3 -3
- package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
- package/gsd-core/workflows/plan-phase.md +41 -73
- package/gsd-core/workflows/plan-review-convergence.md +17 -5
- package/gsd-core/workflows/plant-seed.md +8 -4
- package/gsd-core/workflows/progress.md +6 -2
- package/gsd-core/workflows/quick-batch.md +5 -1
- package/gsd-core/workflows/quick.md +22 -7
- package/gsd-core/workflows/remove-phase.md +5 -1
- package/gsd-core/workflows/resume-project.md +19 -11
- package/gsd-core/workflows/review.md +6 -2
- package/gsd-core/workflows/scan.md +5 -1
- package/gsd-core/workflows/secure-phase.md +6 -2
- package/gsd-core/workflows/settings.md +3 -3
- package/gsd-core/workflows/ship.md +13 -10
- package/gsd-core/workflows/thread.md +10 -2
- package/gsd-core/workflows/transition.md +5 -1
- package/gsd-core/workflows/ui-phase.md +5 -1
- package/gsd-core/workflows/ui-review.md +6 -2
- package/gsd-core/workflows/ultraplan-phase.md +5 -1
- package/gsd-core/workflows/update.md +5 -1
- package/gsd-core/workflows/validate-phase.md +6 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
- package/gsd-core/workflows/verify-work.md +11 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
- package/hooks/dist/gsd-check-update.js +26 -20
- package/hooks/dist/gsd-context-monitor.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
- package/hooks/dist/gsd-secret-read-guard.js +189 -15
- package/hooks/dist/gsd-statusline.js +321 -22
- package/hooks/dist/gsd-validate-commit.sh +27 -5
- package/hooks/gsd-agent-isolation-guard.js +3 -1
- package/hooks/gsd-check-update.js +26 -20
- package/hooks/gsd-context-monitor.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +2 -1
- package/hooks/gsd-secret-read-guard.js +189 -15
- package/hooks/gsd-statusline.js +321 -22
- package/hooks/gsd-validate-commit.sh +27 -5
- package/package.json +1 -1
- package/scripts/audit-workflow-script-paths.cjs +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
- package/scripts/diagnose-validate-commit-phases.cjs +348 -0
- package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
- package/scripts/docs-guard-registry.cjs +2 -0
- package/scripts/gen-capability-registry.cjs +7 -1
- package/scripts/gen-loop-host-contract.cjs +4 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +13 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +26 -0
- package/scripts/lint-phase-arg-assignment.cjs +2 -2
- package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
- package/scripts/mutation-matrix.cjs +3 -1
- package/scripts/release-tarball-smoke.cjs +15 -7
- package/scripts/xtrace-phase-timing.cjs +205 -0
- package/skills/gsd-code-review/SKILL.md +8 -0
- package/skills/gsd-pause-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -32,6 +32,8 @@ const node_os_1 = __importDefault(require("node:os"));
|
|
|
32
32
|
const node_https_1 = __importDefault(require("node:https"));
|
|
33
33
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
34
34
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
|
+
const { resolveGsdHome } = require('./runtime-homes.cjs');
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
37
|
const shellSeam = require('./shell-command-projection.cjs');
|
|
36
38
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
39
|
const capValidator = require('./capability-validator.cjs');
|
|
@@ -984,7 +986,7 @@ async function resolveTarball(parsed, opts, gsdHome, hostVersion) {
|
|
|
984
986
|
async function resolveCapabilitySource(spec, opts = {}) {
|
|
985
987
|
const parsed = parseSpec(spec);
|
|
986
988
|
const hostVersion = opts.hostVersion ?? readHostVersion();
|
|
987
|
-
const gsdHome = opts.gsdHome
|
|
989
|
+
const gsdHome = opts.gsdHome || resolveGsdHome();
|
|
988
990
|
switch (parsed.kind) {
|
|
989
991
|
case 'local':
|
|
990
992
|
return resolveLocal(parsed, opts, gsdHome, hostVersion);
|
|
@@ -47,6 +47,8 @@ const { loadConfig } = configLoaderMod;
|
|
|
47
47
|
const installProfilesMod = require("./install-profiles.cjs");
|
|
48
48
|
const { readActiveProfile, loadSkillsManifest, resolveProfile, parseRequires, parseCallsAgents, workflowAgentRefs } = installProfilesMod;
|
|
49
49
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
+
const ledgerMod = require("./capability-ledger.cjs");
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
52
|
const surfaceMod = require("./surface.cjs");
|
|
51
53
|
const { resolveSurface } = surfaceMod;
|
|
52
54
|
// ─── Prototype-pollution guard (inline literal, CodeQL barrier) ───────────────
|
|
@@ -265,16 +267,17 @@ function _loadInstalledSkillsManifest(configDir) {
|
|
|
265
267
|
// SKILL.md must NOT register the stem — otherwise the capability would be
|
|
266
268
|
// wrongly reported surfaced/enabled and a verify:post hook would render
|
|
267
269
|
// for a skill that cannot run.
|
|
268
|
-
|
|
269
|
-
continue;
|
|
270
|
+
// #5121: bounded read — a missing, non-regular (FIFO/device) or oversized SKILL.md registers no stem.
|
|
270
271
|
let content = '';
|
|
271
272
|
try {
|
|
272
|
-
content =
|
|
273
|
+
content = ledgerMod.readSkillFile(skillMdPath);
|
|
273
274
|
}
|
|
274
275
|
catch {
|
|
275
276
|
// SKILL.md present but unreadable — register with no deps (parity with
|
|
276
277
|
// loadSkillsManifest's readFileSync catch branch).
|
|
277
278
|
}
|
|
279
|
+
if (content === null)
|
|
280
|
+
continue;
|
|
278
281
|
// Parse requires: via the SAME shared parser loadSkillsManifest uses, so
|
|
279
282
|
// installed-runtime dependency resolution can never silently diverge from
|
|
280
283
|
// the source-tree path (single source of truth — no duplicated regex).
|
|
@@ -1899,6 +1899,10 @@ const REVIEWER_EFFORT_LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhi
|
|
|
1899
1899
|
* silently rot away from reality (DEFECT.GENERATIVE-FIX).
|
|
1900
1900
|
*/
|
|
1901
1901
|
const KNOWN_HOST_BEHAVIORS = new Set([
|
|
1902
|
+
// #2586: declares runtime-level feature axes GSD does not/cannot support on
|
|
1903
|
+
// omitting the key on every other runtime carries no inverted meaning.
|
|
1904
|
+
// this host (e.g. Codex's `["context-warnings","phase-lifecycle-display"]`)
|
|
1905
|
+
// — present-and-populated / absent-is-unsupported-empty convention, so
|
|
1902
1906
|
'agentFileExtension',
|
|
1903
1907
|
'agentFrontmatterExtensions',
|
|
1904
1908
|
'agentManifestStyle',
|
|
@@ -1916,6 +1920,7 @@ const KNOWN_HOST_BEHAVIORS = new Set([
|
|
|
1916
1920
|
'combinedFamilyInstall',
|
|
1917
1921
|
'commandBodyConverter',
|
|
1918
1922
|
'contentRewriteProfile',
|
|
1923
|
+
'defaultHomeIncludeRoot',
|
|
1919
1924
|
'doneBannerStyle',
|
|
1920
1925
|
'flatCommandDir',
|
|
1921
1926
|
'frontmatterDialect',
|
|
@@ -1936,6 +1941,7 @@ const KNOWN_HOST_BEHAVIORS = new Set([
|
|
|
1936
1941
|
'managedHookEvents',
|
|
1937
1942
|
'mcpCompanion',
|
|
1938
1943
|
'namedSubagentsSupported',
|
|
1944
|
+
'nativeCliPlugin',
|
|
1939
1945
|
'nativeModelAliases',
|
|
1940
1946
|
'nativePlugin',
|
|
1941
1947
|
'noPathRewrite',
|
|
@@ -1952,6 +1958,7 @@ const KNOWN_HOST_BEHAVIORS = new Set([
|
|
|
1952
1958
|
'requiresSubagentPair',
|
|
1953
1959
|
'restoreAtRefTildeInAgents',
|
|
1954
1960
|
'restoreAtRefTildeInSpecTree',
|
|
1961
|
+
'retargetsGsdCoreProsePath',
|
|
1955
1962
|
'retiredArtifacts',
|
|
1956
1963
|
'rewriteClaudeAtIncludes',
|
|
1957
1964
|
'settingsFileByScope',
|
|
@@ -1971,10 +1978,6 @@ const KNOWN_HOST_BEHAVIORS = new Set([
|
|
|
1971
1978
|
'specRootSkillPass',
|
|
1972
1979
|
'tomlConfigInstall',
|
|
1973
1980
|
'trackCategoryDescription',
|
|
1974
|
-
// #2586: declares runtime-level feature axes GSD does not/cannot support on
|
|
1975
|
-
// this host (e.g. Codex's `["context-warnings","phase-lifecycle-display"]`)
|
|
1976
|
-
// — present-and-populated / absent-is-unsupported-empty convention, so
|
|
1977
|
-
// omitting the key on every other runtime carries no inverted meaning.
|
|
1978
1981
|
'unsupportedFeatures',
|
|
1979
1982
|
'verificationStyle',
|
|
1980
1983
|
'writeCategoryDescription',
|
|
@@ -2572,7 +2575,7 @@ function validateLaneProbe(ctx, probe) {
|
|
|
2572
2575
|
if (!isPositiveIntegerMs(probe.timeoutMs)) {
|
|
2573
2576
|
errors.push(
|
|
2574
2577
|
ctx + ' reviewer.probe.timeoutMs must be a positive integer of milliseconds for kind "' +
|
|
2575
|
-
probe.kind + '" — an unbounded probe hangs every /gsd
|
|
2578
|
+
probe.kind + '" — an unbounded probe hangs every /gsd-review invocation ' +
|
|
2576
2579
|
'(got: ' + describeValue(probe.timeoutMs) + ')',
|
|
2577
2580
|
);
|
|
2578
2581
|
}
|
|
@@ -50,6 +50,7 @@ const { planningDir } = planningWorkspaceMod;
|
|
|
50
50
|
const nodefs = require("fs");
|
|
51
51
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
52
52
|
const nodepath = require("path");
|
|
53
|
+
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
53
54
|
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
54
55
|
/**
|
|
55
56
|
* Write-side capability state mutator.
|
|
@@ -305,7 +306,7 @@ function setCapabilityState(cwd, runtimeConfigDir, desired, opts) {
|
|
|
305
306
|
if (entry.enabled === true) {
|
|
306
307
|
const afterCap = after.capabilities.find((c) => c.id === entry.id);
|
|
307
308
|
if (afterCap && afterCap.enabled !== true) {
|
|
308
|
-
errors.push(`cannot enable "${entry.id}": not in the active surface/profile (widen the profile or use
|
|
309
|
+
errors.push(`cannot enable "${entry.id}": not in the active surface/profile (widen the profile or use ${(0, runtime_slash_cjs_1.runtimeCommand)(cwd, 'surface enable')})`);
|
|
309
310
|
}
|
|
310
311
|
}
|
|
311
312
|
// Fix B: desired enabled=false — assert it is actually disabled after write.
|
|
@@ -1233,6 +1233,24 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1233
1233
|
// null — it would leave an unresolvable `variant: null` key on disk
|
|
1234
1234
|
// forever. Test presence with its own regex, and only compare values
|
|
1235
1235
|
// once presence is known.
|
|
1236
|
+
// Native OpenCode 2.x form (ADR-5331 D5): the effort rides on the model reference as
|
|
1237
|
+
// `provider/model#variant`, with no separate `variant:` key. When the agent's `model:` carries a
|
|
1238
|
+
// `#` suffix that suffix IS the effort, so the sync edits the model instead of adding a second,
|
|
1239
|
+
// conflicting `variant:` line.
|
|
1240
|
+
const modelLineMatch = /^model:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch.body);
|
|
1241
|
+
const modelValue = modelLineMatch ? modelLineMatch[1].replace(/\r$/, '').replace(/^(["'])(.*)\1$/, '$2') : null;
|
|
1242
|
+
const hashAt = modelValue === null ? -1 : modelValue.lastIndexOf('#');
|
|
1243
|
+
let modelFormContent = null;
|
|
1244
|
+
let modelFormFrom = null;
|
|
1245
|
+
if (modelValue !== null && hashAt > 0) {
|
|
1246
|
+
const base = modelValue.slice(0, hashAt);
|
|
1247
|
+
modelFormFrom = modelValue.slice(hashAt + 1);
|
|
1248
|
+
if (target !== null && modelFormFrom === target) {
|
|
1249
|
+
skipped++;
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
modelFormContent = setFrontmatterKeyLine(content, 'model', target === null ? base : `${base}#${target}`);
|
|
1253
|
+
}
|
|
1236
1254
|
const variantPresent = /^variant:/m.test(fmMatch.body);
|
|
1237
1255
|
const variantMatch = /^variant:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch.body);
|
|
1238
1256
|
// `null` (key absent) and `''` (key present, value empty) are distinct
|
|
@@ -1241,17 +1259,19 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1241
1259
|
// "empty key" indistinguishable in the sync output, even though only one
|
|
1242
1260
|
// of them actually has a `variant:` line to remove.
|
|
1243
1261
|
const currentVariant = variantPresent ? (variantMatch ? variantMatch[1] : '') : null;
|
|
1244
|
-
if (
|
|
1245
|
-
if (
|
|
1262
|
+
if (modelFormContent === null) {
|
|
1263
|
+
if (target === null) {
|
|
1264
|
+
if (!variantPresent) {
|
|
1265
|
+
skipped++;
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
else if (variantPresent && currentVariant === target) {
|
|
1246
1270
|
skipped++;
|
|
1247
1271
|
continue;
|
|
1248
1272
|
}
|
|
1249
1273
|
}
|
|
1250
|
-
|
|
1251
|
-
skipped++;
|
|
1252
|
-
continue;
|
|
1253
|
-
}
|
|
1254
|
-
changes.push({ agent: agentName, from: currentVariant, to: target });
|
|
1274
|
+
changes.push({ agent: agentName, from: modelFormContent === null ? currentVariant : modelFormFrom, to: target });
|
|
1255
1275
|
synced++;
|
|
1256
1276
|
if (!dryRun) {
|
|
1257
1277
|
// Atomic publish AND mode preservation, same discipline as
|
|
@@ -1279,7 +1299,7 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1279
1299
|
}
|
|
1280
1300
|
catch { /* non-fatal: fall back to writing without an explicit mode */ }
|
|
1281
1301
|
try {
|
|
1282
|
-
node_fs_1.default.writeFileSync(tmpPath, target === null ? removeFrontmatterKeyLine(content, 'variant') : setFrontmatterKeyLine(content, 'variant', target), originalMode !== undefined ? { mode: originalMode } : undefined);
|
|
1302
|
+
node_fs_1.default.writeFileSync(tmpPath, modelFormContent ?? (target === null ? removeFrontmatterKeyLine(content, 'variant') : setFrontmatterKeyLine(content, 'variant', target)), originalMode !== undefined ? { mode: originalMode } : undefined);
|
|
1283
1303
|
// Not redundant with the `mode` option above: `mode` only applies
|
|
1284
1304
|
// when the file is actually created (O_CREAT). A leftover tmp file
|
|
1285
1305
|
// from an earlier crashed run would be reused (truncated) at its OLD
|
|
@@ -2966,6 +2986,8 @@ async function cmdWebsearch(query, options, raw) {
|
|
|
2966
2986
|
'Accept': 'application/json',
|
|
2967
2987
|
'X-Subscription-Token': apiKey
|
|
2968
2988
|
},
|
|
2989
|
+
// never follow redirects: X-Subscription-Token must not leave api.search.brave.com (#5409)
|
|
2990
|
+
redirect: 'manual',
|
|
2969
2991
|
signal: ac.signal
|
|
2970
2992
|
});
|
|
2971
2993
|
}
|
|
@@ -2991,7 +3013,9 @@ async function cmdWebsearch(query, options, raw) {
|
|
|
2991
3013
|
const status = response.status;
|
|
2992
3014
|
const isRetryable = status === 429 || status >= 500;
|
|
2993
3015
|
if (!isRetryable) {
|
|
2994
|
-
// Non-retryable 4xx
|
|
3016
|
+
// Non-retryable 4xx and every 3xx (redirect: 'manual' hands them back as-is, #5409) —
|
|
3017
|
+
// fail immediately, no attempts field. Keep 3xx out of isRetryable.
|
|
3018
|
+
response.body?.cancel().catch(() => { });
|
|
2995
3019
|
output({ available: false, error: `API error: ${status}` }, raw, '');
|
|
2996
3020
|
return;
|
|
2997
3021
|
}
|
|
@@ -25,11 +25,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
26
|
};
|
|
27
27
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
28
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
29
28
|
const node_path_1 = __importDefault(require("node:path"));
|
|
30
29
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
31
30
|
// #4717: runtime-identity fill — env rung + per-install marker rung.
|
|
32
31
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
32
|
+
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
33
33
|
const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
|
|
34
34
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
35
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
@@ -935,7 +935,7 @@ function loadConfigResolvedInternal(cwd, options = {}) {
|
|
|
935
935
|
// untouched. Faults in the global file stay silent in this branch (the
|
|
936
936
|
// project config governs; the nearer file is the actionable one).
|
|
937
937
|
try {
|
|
938
|
-
const shadowHome =
|
|
938
|
+
const shadowHome = (0, runtime_homes_cjs_1.resolveGsdHome)();
|
|
939
939
|
const shadowPath = node_path_1.default.join(shadowHome, '.gsd', 'defaults.json');
|
|
940
940
|
const shadowRead = _readConfigFile(shadowPath);
|
|
941
941
|
if (shadowRead.kind === 'ok') {
|
|
@@ -997,7 +997,7 @@ function loadConfigResolvedInternal(cwd, options = {}) {
|
|
|
997
997
|
}
|
|
998
998
|
// Branch D or E: no .planning/
|
|
999
999
|
try {
|
|
1000
|
-
const home =
|
|
1000
|
+
const home = (0, runtime_homes_cjs_1.resolveGsdHome)();
|
|
1001
1001
|
const globalDefaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
|
|
1002
1002
|
const globalRead = _readConfigFile(globalDefaultsPath);
|
|
1003
1003
|
if (globalRead.kind === 'fault') {
|
|
@@ -21,6 +21,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
21
21
|
};
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.decisionMentioned = decisionMentioned;
|
|
24
|
+
exports.phaseDirProblem = phaseDirProblem;
|
|
24
25
|
exports.loadPlanContents = loadPlanContents;
|
|
25
26
|
exports.loadSummaryContents = loadSummaryContents;
|
|
26
27
|
exports.loadDecisionExtraction = loadDecisionExtraction;
|
|
@@ -70,6 +71,24 @@ function decisionMentioned(haystack, decision) {
|
|
|
70
71
|
return phrase ? normalizePhrase(haystack).includes(phrase) : false;
|
|
71
72
|
}
|
|
72
73
|
// ─── File reading ─────────────────────────────────────────────────────────────
|
|
74
|
+
// #4939: loadPlanContents answers no files for a path that is not there, and that reads
|
|
75
|
+
// as "no plan cites anything" — so a phase-dir argument that names no directory
|
|
76
|
+
// (the phase NUMBER in the phase-dir slot) must be told apart BEFORE the scan.
|
|
77
|
+
// Answers null for a directory, else what is wrong with the path and what to do
|
|
78
|
+
// about it. Like the plan gate's contextKind (#4794), the message names the real
|
|
79
|
+
// stat outcome: a permission or I/O failure (EACCES, ELOOP, EIO) is reported as
|
|
80
|
+
// unreadable, not as a missing directory. Only ENOENT/ENOTDIR mean the path is not there.
|
|
81
|
+
function phaseDirProblem(dirPath) {
|
|
82
|
+
const passDirectory = 'Pass the phase DIRECTORY (e.g. .planning/phases/01-slug), not the phase number.';
|
|
83
|
+
const st = (0, gate_evidence_cjs_1.statEvidence)(dirPath);
|
|
84
|
+
if (st.kind === 'none')
|
|
85
|
+
return { reason: 'phase directory not found', what: 'does not exist', hint: passDirectory, unreadable: false };
|
|
86
|
+
if (st.kind === 'unreadable')
|
|
87
|
+
return { reason: 'phase directory unreadable', what: `could not be read (${st.reason})`, hint: 'Check that the directory is readable.', unreadable: true };
|
|
88
|
+
if (st.value.isDirectory())
|
|
89
|
+
return null;
|
|
90
|
+
return { reason: 'phase path is not a directory', what: st.value.isFile() ? 'is a file, not a directory' : 'is not a directory', hint: passDirectory, unreadable: false };
|
|
91
|
+
}
|
|
73
92
|
/**
|
|
74
93
|
* The contents of the phase files `pick` selects (#5170, ADR-5057 §4). An ABSENT phase directory
|
|
75
94
|
* is "no files" (`found []`); a directory or file that exists but cannot be read is `unreadable` —
|
|
@@ -28,6 +28,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
28
28
|
const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
|
|
29
29
|
const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
|
|
30
30
|
const gate_config_cjs_1 = require("./gate-config.cjs");
|
|
31
|
+
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
31
32
|
const gate_phase_context_cjs_1 = require("./gate-phase-context.cjs");
|
|
32
33
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
|
33
34
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
@@ -45,13 +46,13 @@ const { defaultPhaseCleanCommitTimesMs } = verificationMod;
|
|
|
45
46
|
function computeContextDrift(contextEffectiveMs, entries) {
|
|
46
47
|
return entries.filter((e) => e.effectiveMs < contextEffectiveMs).map((e) => e.file);
|
|
47
48
|
}
|
|
48
|
-
function buildContextDriftMessage(staleArtifacts, phaseArg) {
|
|
49
|
+
function buildContextDriftMessage(staleArtifacts, phaseArg, projectDir) {
|
|
49
50
|
const parts = [`CONTEXT.md decisions are newer than: ${staleArtifacts.join(', ')}.`];
|
|
50
51
|
if (staleArtifacts.some((f) => f.endsWith('-RESEARCH.md'))) {
|
|
51
|
-
parts.push(`Regenerate research:
|
|
52
|
+
parts.push(`Regenerate research: ${(0, runtime_slash_cjs_1.runtimeCommand)(projectDir, `plan-phase ${phaseArg} --research`)}.`);
|
|
52
53
|
}
|
|
53
54
|
if (staleArtifacts.some((f) => f.endsWith('-PATTERNS.md'))) {
|
|
54
|
-
parts.push(
|
|
55
|
+
parts.push(`Regenerate patterns: delete the PATTERNS.md file, then re-run ${(0, runtime_slash_cjs_1.runtimeCommand)(projectDir, 'plan-phase')}.`);
|
|
55
56
|
}
|
|
56
57
|
if (staleArtifacts.some((f) => f.endsWith('-VALIDATION.md') || (f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md')))) {
|
|
57
58
|
parts.push('Regenerate or manually reconcile VALIDATION.md / SPEC.md against the current decisions.');
|
|
@@ -114,7 +115,7 @@ function runContextDriftGate(cwd, phaseArg) {
|
|
|
114
115
|
// missing or malformed config is "key absent", nothing is printed).
|
|
115
116
|
const action = (0, gate_config_cjs_1.readWorkflowConfigValue)(cwd, 'workflow.context_drift_action').value === 'block' ? 'block' : 'warn';
|
|
116
117
|
const block = staleArtifacts.length > 0 && action === 'block';
|
|
117
|
-
const message = staleArtifacts.length > 0 ? buildContextDriftMessage(staleArtifacts, phaseArg) : '';
|
|
118
|
+
const message = staleArtifacts.length > 0 ? buildContextDriftMessage(staleArtifacts, phaseArg, cwd) : '';
|
|
118
119
|
return (0, gate_verdict_cjs_1.gateVerdict)(block ? 'block' : (staleArtifacts.length > 0 ? 'advisory' : 'pass'), block, {
|
|
119
120
|
block,
|
|
120
121
|
skipped: false,
|
|
@@ -145,9 +145,24 @@ function evaluateDecisionCoveragePlan(input) {
|
|
|
145
145
|
if (decisions.length === 0) {
|
|
146
146
|
return (0, gate_verdict_cjs_1.gateVerdict)('skip', false, { passed: true, skipped: true, reason: 'no trackable decisions', total: 0, covered: 0, uncovered: [], message: 'No trackable decisions in CONTEXT.md.' });
|
|
147
147
|
}
|
|
148
|
+
// #4939: a phase-dir argument that was GIVEN but is not a directory is a caller
|
|
149
|
+
// error like #2770's empty context argument and #4794's non-file context path:
|
|
150
|
+
// no plan was scanned, so nothing was measured. Fail closed with total/covered
|
|
151
|
+
// null and `uncovered` omitted. An OMITTED phase dir is left to the scan below —
|
|
152
|
+
// the #4130 `--context <path>` alone tests pin that as a coverage gap.
|
|
153
|
+
const phaseDirIssue = phaseDir ? (0, decision_coverage_support_cjs_1.phaseDirProblem)(phaseDir) : null;
|
|
154
|
+
if (phaseDirIssue) {
|
|
155
|
+
const phaseDirResult = { passed: false, skipped: false, reason: phaseDirIssue.reason, total: null, covered: null, message: `Decision coverage gate: the phase directory "${positionals[0]}" ${phaseDirIssue.what}, so no plans were scanned. ${phaseDirIssue.hint}` };
|
|
156
|
+
return phaseDirIssue.unreadable ? (0, gate_verdict_cjs_1.gateUnreadable)(true, phaseDirResult) : (0, gate_verdict_cjs_1.gateVerdict)('block', true, phaseDirResult);
|
|
157
|
+
}
|
|
148
158
|
const planContents = (0, decision_coverage_support_cjs_1.loadPlanContents)(phaseDir);
|
|
149
159
|
if (planContents.kind === 'unreadable')
|
|
150
160
|
return unreadableDecisionGate(`${planContents.span ?? phaseDir}: ${planContents.reason}`);
|
|
161
|
+
// #4939: a phase directory that holds no PLAN file is the same caller error: zero plans
|
|
162
|
+
// loaded is not zero coverage.
|
|
163
|
+
if (phaseDir && planContents.value.length === 0) {
|
|
164
|
+
return (0, gate_verdict_cjs_1.gateVerdict)('block', true, { passed: false, skipped: false, reason: 'phase directory holds no plans', total: null, covered: null, message: `Decision coverage gate: the phase directory "${positionals[0]}" holds no PLAN files, so no plans were scanned. Pass the phase DIRECTORY that holds the plans (e.g. .planning/phases/01-slug), not the phase number.` });
|
|
165
|
+
}
|
|
151
166
|
const sections = planContents.value.map(decision_coverage_support_cjs_1.extractPlanDesignatedSections);
|
|
152
167
|
const uncovered = [];
|
|
153
168
|
let covered = 0;
|
|
@@ -90,6 +90,14 @@ function evaluateDecisionCoverageVerify(input) {
|
|
|
90
90
|
if (decisions.length === 0) {
|
|
91
91
|
return (0, gate_verdict_cjs_1.gateVerdict)('skip', false, { skipped: true, blocking: false, reason: 'no trackable decisions', total: 0, honored: 0, not_honored: [], message: 'No trackable decisions in CONTEXT.md.' });
|
|
92
92
|
}
|
|
93
|
+
// #4939: mirror the plan gate. A phase-dir argument that names no directory
|
|
94
|
+
// means no plan or summary was scanned, so no decision can be reported
|
|
95
|
+
// not-honored. Warn (verify stays non-blocking) rather than list every decision.
|
|
96
|
+
const phaseDirIssue = phaseDir ? (0, decision_coverage_support_cjs_1.phaseDirProblem)(phaseDir) : null;
|
|
97
|
+
if (phaseDirIssue) {
|
|
98
|
+
const phaseDirResult = { skipped: false, blocking: false, reason: phaseDirIssue.reason, total: null, honored: null, not_honored: [], message: `Decision coverage verify (warning): the phase directory "${args[0]}" ${phaseDirIssue.what}, so no plans or summaries were scanned. ${phaseDirIssue.hint}` };
|
|
99
|
+
return phaseDirIssue.unreadable ? (0, gate_verdict_cjs_1.gateUnreadable)(false, phaseDirResult) : (0, gate_verdict_cjs_1.gateVerdict)('advisory', false, phaseDirResult);
|
|
100
|
+
}
|
|
93
101
|
// Every file the haystack is built from is typed evidence (#5170): one that exists but cannot be
|
|
94
102
|
// read is `unreadable`, never an empty slice of the haystack that makes a decision look unhonored.
|
|
95
103
|
const planContents = (0, decision_coverage_support_cjs_1.loadPlanContents)(phaseDir);
|
|
@@ -98,6 +106,11 @@ function evaluateDecisionCoverageVerify(input) {
|
|
|
98
106
|
const summaryParts = (0, decision_coverage_support_cjs_1.loadSummaryContents)(phaseDir);
|
|
99
107
|
if (summaryParts.kind === 'unreadable')
|
|
100
108
|
return unreadableVerify(`${summaryParts.span ?? phaseDir}: ${summaryParts.reason}`);
|
|
109
|
+
// #4939: mirror the plan gate. A phase directory with no PLAN and no SUMMARY file gave the
|
|
110
|
+
// verifier nothing to read, which is a caller error, not a phase whose decisions were all missed.
|
|
111
|
+
if (phaseDir && planContents.value.length === 0 && summaryParts.value.length === 0) {
|
|
112
|
+
return (0, gate_verdict_cjs_1.gateVerdict)('advisory', false, { skipped: false, blocking: false, reason: 'phase directory holds no plans', total: null, honored: null, not_honored: [], message: `Decision coverage verify (warning): the phase directory "${args[0]}" holds no PLAN or SUMMARY files, so nothing was scanned. Pass the phase DIRECTORY that holds the plans (e.g. .planning/phases/01-slug), not the phase number.` });
|
|
113
|
+
}
|
|
101
114
|
const modifiedFiles = (0, decision_coverage_support_cjs_1.readModifiedFilesContent)(projectDir, summaryParts.value);
|
|
102
115
|
if (modifiedFiles.kind === 'unreadable')
|
|
103
116
|
return unreadableVerify(`${modifiedFiles.span ?? 'files_modified'}: ${modifiedFiles.reason}`);
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
* This is a leaf pure module: no fs, no child_process, no config — the subprocess
|
|
27
27
|
* seam is injected so the evaluator is trivially testable without spawning.
|
|
28
28
|
*/
|
|
29
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
30
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
|
+
};
|
|
32
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
29
33
|
// ─── Public constants ─────────────────────────────────────────────────────────
|
|
30
34
|
/** Default command timeout for `command-exit-zero` (30s). Matches execTool default. */
|
|
31
35
|
const COMMAND_EXIT_ZERO_DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -116,15 +120,31 @@ function evaluateArtifactFrontmatterEquals(predicate, ctx, deps) {
|
|
|
116
120
|
throw new Error('artifact-frontmatter-equals predicate requires an "equals" key');
|
|
117
121
|
}
|
|
118
122
|
const targetDir = isNonEmptyString(ctx.phaseDir) ? ctx.phaseDir : ctx.cwd;
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
// Plain `.sort()` (no locale) so the outcome never depends on directory listing order (#5313).
|
|
124
|
+
// Set: finders return realpaths, so a symlink to the same file is one artifact, not two.
|
|
125
|
+
const found = deps.findPhaseArtifact(targetDir, artifactSuffix);
|
|
126
|
+
// A pre-#5313 dep returned `string | null`; a silent null -> "not found" block or per-character spread would mislead.
|
|
127
|
+
if (!Array.isArray(found)) {
|
|
128
|
+
throw new Error('predicate deps "findPhaseArtifact" must return an array of paths');
|
|
129
|
+
}
|
|
130
|
+
const candidates = [...new Set(found)].sort();
|
|
131
|
+
if (candidates.length === 0) {
|
|
121
132
|
return {
|
|
122
133
|
block: true,
|
|
123
134
|
message: `Artifact matching ${artifactSuffix} not found in ${targetDir}`,
|
|
124
135
|
details: { kind: 'artifact-frontmatter-equals', artifactNotFound: true },
|
|
125
136
|
};
|
|
126
137
|
}
|
|
127
|
-
|
|
138
|
+
// #5313: never pick one of several matches (listing order is OS-dependent); a throw would fail open via onError.
|
|
139
|
+
if (candidates.length > 1) {
|
|
140
|
+
const names = candidates.map((c) => node_path_1.default.relative(targetDir, c).split(node_path_1.default.sep).join('/')).join(', ');
|
|
141
|
+
return {
|
|
142
|
+
block: true,
|
|
143
|
+
message: `Multiple artifacts match ${artifactSuffix} in ${targetDir}: ${names}; remove or rename all but one`,
|
|
144
|
+
details: { kind: 'artifact-frontmatter-equals', ambiguous: true, candidates },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const fm = deps.readFrontmatter(candidates[0]);
|
|
128
148
|
const actualValue = fm[field];
|
|
129
149
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
130
150
|
const expectedStr = typeof expectedValue === 'object' ? JSON.stringify(expectedValue) : String(expectedValue);
|
|
@@ -80,31 +80,27 @@ function buildPredicateDeps() {
|
|
|
80
80
|
if (phaseDirStat.kind === 'unreadable')
|
|
81
81
|
return unreadable(phaseDir, phaseDirStat.reason);
|
|
82
82
|
if (phaseDirStat.kind === 'none')
|
|
83
|
-
return
|
|
83
|
+
return [];
|
|
84
84
|
if (artifactSuffix === '.' ||
|
|
85
85
|
artifactSuffix === '..' ||
|
|
86
86
|
artifactSuffix.includes('\0') ||
|
|
87
87
|
node_path_1.default.basename(artifactSuffix) !== artifactSuffix ||
|
|
88
88
|
node_path_1.default.win32.basename(artifactSuffix) !== artifactSuffix) {
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
const directContained = (0, security_cjs_1.tryWithinRoot)(artifactSuffix, phaseDir);
|
|
92
|
-
if (directContained !== null) {
|
|
93
|
-
const direct = regularFileOrNull(directContained);
|
|
94
|
-
if (direct !== null)
|
|
95
|
-
return direct;
|
|
89
|
+
return [];
|
|
96
90
|
}
|
|
91
|
+
// #5313: collect EVERY distinct match; the evaluator fails closed on >1 instead of trusting listing order.
|
|
92
|
+
const found = [];
|
|
97
93
|
const planningContained = (0, security_cjs_1.tryWithinRoot)(node_path_1.default.join('.planning', artifactSuffix), phaseDir);
|
|
98
94
|
if (planningContained !== null) {
|
|
99
95
|
const planning = regularFileOrNull(planningContained);
|
|
100
96
|
if (planning !== null)
|
|
101
|
-
|
|
97
|
+
found.push(planning);
|
|
102
98
|
}
|
|
103
99
|
const listing = (0, gate_evidence_cjs_1.readDirEvidence)(phaseDir);
|
|
104
100
|
if (listing.kind === 'unreadable')
|
|
105
101
|
return unreadable(phaseDir, listing.reason);
|
|
106
102
|
if (listing.kind === 'none')
|
|
107
|
-
return
|
|
103
|
+
return [];
|
|
108
104
|
for (const f of listing.value) {
|
|
109
105
|
if (f.endsWith('-' + artifactSuffix) || f === artifactSuffix) {
|
|
110
106
|
const candidateContained = (0, security_cjs_1.tryWithinRoot)(f, phaseDir);
|
|
@@ -112,10 +108,10 @@ function buildPredicateDeps() {
|
|
|
112
108
|
continue;
|
|
113
109
|
const candidate = regularFileOrNull(candidateContained);
|
|
114
110
|
if (candidate !== null)
|
|
115
|
-
|
|
111
|
+
found.push(candidate);
|
|
116
112
|
}
|
|
117
113
|
}
|
|
118
|
-
return
|
|
114
|
+
return found;
|
|
119
115
|
},
|
|
120
116
|
readFrontmatter(filePath) {
|
|
121
117
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
* tolerant. A plan id that is not `<phase>-<plan>` (a plan named `.*-PLAN.md`) matches nothing,
|
|
18
18
|
* and a git failure is "no commits" exactly as before.
|
|
19
19
|
*
|
|
20
|
+
* Cross-plan GREEN (#4867): a gap closure is planned as a RED-only probe plan plus the plan that
|
|
21
|
+
* `depends_on` it and carries the fix. A `type: tdd` plan with its own RED but no GREEN is credited
|
|
22
|
+
* with a `feat(...)` commit under any plan that lists it in `depends_on`, directly or through a
|
|
23
|
+
* chain; the row carries `greenFrom` and the report names the supplier. Its own RED is still
|
|
24
|
+
* required, and a plan nothing depends on is scored on its own commits alone.
|
|
25
|
+
*
|
|
20
26
|
* Argv after the verb: `<phase>` (a number; an unresolvable phase reports zero plans).
|
|
21
27
|
*/
|
|
22
28
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
@@ -31,7 +37,7 @@ const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
|
|
|
31
37
|
const gate_evaluation_scope_cjs_1 = require("./gate-evaluation-scope.cjs");
|
|
32
38
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
33
39
|
const frontmatterMod = require("./frontmatter.cjs");
|
|
34
|
-
const { frontmatterKeyHasValue } = frontmatterMod;
|
|
40
|
+
const { frontmatterKeyHasValue, extractFrontmatter } = frontmatterMod;
|
|
35
41
|
/**
|
|
36
42
|
* True when the plan's frontmatter declares `type: tdd` (CRLF included, #2449). The block is the
|
|
37
43
|
* one fence owner's, read as RAW text — not through the YAML parser — so a block the parser refuses
|
|
@@ -42,6 +48,35 @@ const { frontmatterKeyHasValue } = frontmatterMod;
|
|
|
42
48
|
function isTddPlan(content) {
|
|
43
49
|
return frontmatterKeyHasValue(content, 'type', 'tdd');
|
|
44
50
|
}
|
|
51
|
+
/** A `depends_on` token as a plan id: `01.1-20`, `01.1-20-PLAN` and `01.1-20-PLAN.md` all name plan `01.1-20`. */
|
|
52
|
+
function toPlanId(token) {
|
|
53
|
+
return token.trim().replace(/-PLAN(?:\.md)?$/i, '');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The plan ids a plan lists in `depends_on` (#4867). A plan whose frontmatter does not parse, or has
|
|
57
|
+
* no `depends_on`, depends on nothing: the pairing below only ever ADDS credit, so an unreadable
|
|
58
|
+
* link can never turn a failing plan into a passing one.
|
|
59
|
+
*/
|
|
60
|
+
function dependsOnOf(content) {
|
|
61
|
+
// `extractFrontmatter` does not throw: an unparseable block is an empty result, so `depends_on` is absent.
|
|
62
|
+
const value = extractFrontmatter(content)['depends_on'];
|
|
63
|
+
const tokens = Array.isArray(value) ? value.map(String) : typeof value === 'string' ? value.split(',') : [];
|
|
64
|
+
return tokens.map(toPlanId).filter((id) => id !== '');
|
|
65
|
+
}
|
|
66
|
+
/** Every plan that depends on `planId`, directly or through a chain (#4867); a cycle is walked once. */
|
|
67
|
+
function transitiveDependents(planId, dependents) {
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
const queue = [planId];
|
|
70
|
+
while (queue.length > 0) {
|
|
71
|
+
for (const next of dependents.get(queue.shift()) ?? []) {
|
|
72
|
+
if (next !== planId && !seen.has(next)) {
|
|
73
|
+
seen.add(next);
|
|
74
|
+
queue.push(next);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return [...seen].sort();
|
|
79
|
+
}
|
|
45
80
|
/**
|
|
46
81
|
* The commit types (`test`, `feat`, `refactor`, …) among the plan's own commits — those that
|
|
47
82
|
* touched at least one path (`pathspecs: ['.']`, as the old `git log -- .` lookup did). An
|
|
@@ -91,6 +126,9 @@ function evaluateTddReviewCheckpoint(input) {
|
|
|
91
126
|
const phaseDir = located.kind === 'found' ? located.value : '';
|
|
92
127
|
// Find all PLAN.md files with type: tdd in frontmatter
|
|
93
128
|
const tddPlanFiles = [];
|
|
129
|
+
// #4867: plan id -> the plans that list it in `depends_on`, over EVERY plan of the phase (a
|
|
130
|
+
// dependent GREEN plan is usually `type: execute`, not `tdd`).
|
|
131
|
+
const dependents = new Map();
|
|
94
132
|
const unreadable = [];
|
|
95
133
|
if (phaseDir) {
|
|
96
134
|
const entries = (0, gate_evidence_cjs_1.readDirEvidence)(phaseDir);
|
|
@@ -110,8 +148,13 @@ function evaluateTddReviewCheckpoint(input) {
|
|
|
110
148
|
const plan = (0, gate_evidence_cjs_1.readTextEvidence)(planPath);
|
|
111
149
|
if (plan.kind === 'unreadable')
|
|
112
150
|
unreadable.push({ source: planPath, reason: plan.reason });
|
|
113
|
-
else if (plan.kind === 'found'
|
|
114
|
-
|
|
151
|
+
else if (plan.kind === 'found') {
|
|
152
|
+
if (isTddPlan(plan.value))
|
|
153
|
+
tddPlanFiles.push(planPath);
|
|
154
|
+
const planId = node_path_1.default.basename(planPath, '-PLAN.md');
|
|
155
|
+
for (const dep of dependsOnOf(plan.value))
|
|
156
|
+
dependents.set(dep, [...(dependents.get(dep) ?? []), planId]);
|
|
157
|
+
}
|
|
115
158
|
}
|
|
116
159
|
}
|
|
117
160
|
}
|
|
@@ -136,15 +179,24 @@ function evaluateTddReviewCheckpoint(input) {
|
|
|
136
179
|
const planId = node_path_1.default.basename(planPath, '-PLAN.md');
|
|
137
180
|
const kinds = planCommitKinds(projectDir, planId);
|
|
138
181
|
const red = kinds.has('test');
|
|
139
|
-
const green = kinds.has('feat');
|
|
140
182
|
const refactor = kinds.has('refactor');
|
|
183
|
+
// #4867: a RED-only probe plan is satisfied by the GREEN of a plan that (transitively) declares
|
|
184
|
+
// `depends_on` it. Only a real `feat(...)` commit under such a plan counts; the plan's own RED is
|
|
185
|
+
// still required, and a plan nothing depends on is scored on its own commits alone.
|
|
186
|
+
const ownGreen = kinds.has('feat');
|
|
187
|
+
const greenFrom = ownGreen
|
|
188
|
+
? []
|
|
189
|
+
: transitiveDependents(planId, dependents).filter((dependentId) => planCommitKinds(projectDir, dependentId).has('feat'));
|
|
190
|
+
const green = ownGreen || greenFrom.length > 0;
|
|
141
191
|
const missing = [];
|
|
142
192
|
if (!red)
|
|
143
193
|
missing.push('RED');
|
|
144
194
|
if (!green)
|
|
145
195
|
missing.push('GREEN');
|
|
146
196
|
const status = missing.length === 0 ? 'Pass' : 'FAIL';
|
|
147
|
-
rows.push(
|
|
197
|
+
rows.push(greenFrom.length > 0
|
|
198
|
+
? { planId, red, green, refactor, status, missing, greenFrom }
|
|
199
|
+
: { planId, red, green, refactor, status, missing });
|
|
148
200
|
}
|
|
149
201
|
const violations = rows.filter(r => r.status === 'FAIL').length;
|
|
150
202
|
// Build review table
|
|
@@ -160,6 +212,9 @@ function evaluateTddReviewCheckpoint(input) {
|
|
|
160
212
|
tableDivider,
|
|
161
213
|
...tableRows,
|
|
162
214
|
].join('\n');
|
|
215
|
+
for (const r of rows.filter(row => row.greenFrom)) {
|
|
216
|
+
table += `\n\nPlan ${r.planId} GREEN is supplied by dependent plan ${(r.greenFrom ?? []).join(', ')}.`;
|
|
217
|
+
}
|
|
163
218
|
if (violations > 0) {
|
|
164
219
|
table += '\n\n⚠ Gate violations are advisory — review before advancing.';
|
|
165
220
|
for (const r of rows.filter(row => row.status === 'FAIL')) {
|
|
@@ -19,6 +19,7 @@ exports.evaluateUiSafetyGate = evaluateUiSafetyGate;
|
|
|
19
19
|
const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
|
|
20
20
|
const gate_phase_context_cjs_1 = require("./gate-phase-context.cjs");
|
|
21
21
|
const gate_evaluation_scope_cjs_1 = require("./gate-evaluation-scope.cjs");
|
|
22
|
+
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
22
23
|
const ui_safety_gate_cjs_1 = require("./ui-safety-gate.cjs");
|
|
23
24
|
const UI_FILE_EXTENSIONS_RE = /\.(tsx|jsx|css|scss|sass|less|vue|svelte|html)$/i;
|
|
24
25
|
const UI_PATH_PATTERNS_RE = /\/(components|pages|views|screens|layouts|ui|frontend)\//i;
|
|
@@ -52,7 +53,7 @@ function computeUiSafetyGate(projectDir, phase) {
|
|
|
52
53
|
const result = { frontend, hasUiFiles, hasUiSpec, block };
|
|
53
54
|
if (block) {
|
|
54
55
|
result.message = `UI files changed in this wave but no UI-SPEC.md exists for Phase ${phase}. ` +
|
|
55
|
-
`Run
|
|
56
|
+
`Run ${(0, runtime_slash_cjs_1.runtimeCommand)(projectDir, `ui-phase ${phase}`)} to generate the design contract before continuing.`;
|
|
56
57
|
}
|
|
57
58
|
if (phaseLookupFailed)
|
|
58
59
|
result.phaseLookupFailed = true;
|
|
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
15
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
|
+
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
16
17
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
18
|
const capabilityStateMod = require("./capability-state.cjs");
|
|
18
19
|
const { isCapabilityActive } = capabilityStateMod;
|
|
@@ -408,7 +409,7 @@ function graphifyQuery(cwd, term, options = {}) {
|
|
|
408
409
|
const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
|
|
409
410
|
if (!node_fs_1.default.existsSync(graphPath)) {
|
|
410
411
|
return { error: configured
|
|
411
|
-
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run
|
|
412
|
+
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run ${(0, runtime_slash_cjs_1.runtimeCommand)(cwd, 'graphify build')}.`
|
|
412
413
|
: 'No graph built yet. Run graphify build first.' };
|
|
413
414
|
}
|
|
414
415
|
const graph = safeReadJson(graphPath);
|
|
@@ -446,7 +447,7 @@ function graphifyStatus(cwd) {
|
|
|
446
447
|
const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
|
|
447
448
|
if (!node_fs_1.default.existsSync(graphPath)) {
|
|
448
449
|
return { exists: false, graph_path: graphPath, message: configured
|
|
449
|
-
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run
|
|
450
|
+
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run ${(0, runtime_slash_cjs_1.runtimeCommand)(cwd, 'graphify build')}.`
|
|
450
451
|
: 'No graph built yet. Run graphify build to create one.' };
|
|
451
452
|
}
|
|
452
453
|
const stat = node_fs_1.default.statSync(graphPath);
|