@opengsd/gsd-core 1.9.0 → 1.10.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 +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +131 -34
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +503 -341
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +607 -63
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +106 -51
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -25
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +29 -5
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -145
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/registry-schema.cjs +323 -94
- package/scripts/run-tests.cjs +3 -2
- package/scripts/validate-registry.cjs +10 -6
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
package/gsd-core/bin/lib/uat.cjs
CHANGED
|
@@ -22,7 +22,7 @@ const markdownSectionizer = require("./markdown-sectionizer.cjs");
|
|
|
22
22
|
const { collectSection, tokenizeHeadings } = markdownSectionizer;
|
|
23
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
24
|
const markdownTable = require("./markdown-table.cjs");
|
|
25
|
-
const { splitTableRow } = markdownTable;
|
|
25
|
+
const { splitTableRow, isDelimiterRow } = markdownTable;
|
|
26
26
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
27
|
const roadmapParser = require("./roadmap-parser.cjs");
|
|
28
28
|
const { getMilestonePhaseFilter } = roadmapParser;
|
|
@@ -38,6 +38,9 @@ const { extractFrontmatter } = frontmatter;
|
|
|
38
38
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
39
|
const phaseIdMod = require("./phase-id.cjs");
|
|
40
40
|
const { PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
|
+
const phaseLocator = require("./phase-locator.cjs");
|
|
43
|
+
const { getArchivedPhaseDirs } = phaseLocator;
|
|
41
44
|
const security_cjs_1 = require("./security.cjs");
|
|
42
45
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
|
|
43
46
|
const configLoader = require("./config-loader.cjs");
|
|
@@ -45,21 +48,50 @@ const { loadConfig } = configLoader;
|
|
|
45
48
|
// ─── cmdAuditUat ─────────────────────────────────────────────────────────────
|
|
46
49
|
function cmdAuditUat(cwd, raw) {
|
|
47
50
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
48
|
-
|
|
51
|
+
const hasActivePhases = node_fs_1.default.existsSync(phasesDir);
|
|
52
|
+
// #2766: on milestone completion `milestone.cts` MOVES each phase dir into
|
|
53
|
+
// `.planning/milestones/<version>-phases/` (archive-by-default since #1871),
|
|
54
|
+
// leaving `.planning/phases/` empty or absent. Scanning only the active tree
|
|
55
|
+
// meant a partly-archived project silently omitted the archived phases, and a
|
|
56
|
+
// fully-archived one hard-errored with "No phases directory found" —
|
|
57
|
+
// indistinguishable from a broken install. Outstanding UAT items do not stop
|
|
58
|
+
// mattering when a milestone closes: a deferred human-UAT scenario or a
|
|
59
|
+
// `skipped` live-stack test is exactly what gets archived still-open.
|
|
60
|
+
//
|
|
61
|
+
// Reuses the canonical `getArchivedPhaseDirs` seam (phase-locator.cts), which
|
|
62
|
+
// `findPhaseInternal` already uses for this same fallback, so the archive
|
|
63
|
+
// layout convention stays owned by one module.
|
|
64
|
+
const archivedDirs = getArchivedPhaseDirs(cwd);
|
|
65
|
+
if (!hasActivePhases && archivedDirs.length === 0) {
|
|
49
66
|
error('No phases directory found in planning directory');
|
|
50
67
|
}
|
|
51
68
|
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
52
69
|
const results = [];
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
// Active dirs are milestone-filtered; archived dirs deliberately are NOT.
|
|
71
|
+
// getMilestonePhaseFilter derives the CURRENT milestone's phase numbers from
|
|
72
|
+
// ROADMAP.md, and archived phases belong to past milestones by definition — so
|
|
73
|
+
// applying it to them discards every one and silently reinstates the bug.
|
|
74
|
+
const scanTargets = [];
|
|
75
|
+
if (hasActivePhases) {
|
|
76
|
+
const dirs = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true })
|
|
77
|
+
.filter(e => e.isDirectory())
|
|
78
|
+
.map(e => e.name)
|
|
79
|
+
.filter(isDirInMilestone)
|
|
80
|
+
.sort();
|
|
81
|
+
for (const dir of dirs) {
|
|
82
|
+
scanTargets.push({ dir, phaseDir: node_path_1.default.join(phasesDir, dir) });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const archived of archivedDirs) {
|
|
86
|
+
scanTargets.push({
|
|
87
|
+
dir: archived.name,
|
|
88
|
+
phaseDir: archived.fullPath,
|
|
89
|
+
milestone: archived.milestone,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
for (const { dir, phaseDir, milestone } of scanTargets) {
|
|
60
93
|
const phaseMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
|
|
61
94
|
const phaseNum = phaseMatch ? phaseMatch[1] : dir;
|
|
62
|
-
const phaseDir = node_path_1.default.join(phasesDir, dir);
|
|
63
95
|
const files = node_fs_1.default.readdirSync(phaseDir);
|
|
64
96
|
// Process UAT files
|
|
65
97
|
for (const file of files.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
|
|
@@ -74,6 +106,7 @@ function cmdAuditUat(cwd, raw) {
|
|
|
74
106
|
file_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(phaseDir, file))),
|
|
75
107
|
type: 'uat',
|
|
76
108
|
status: (extractFrontmatter(content, uatFilePath).status || 'unknown'),
|
|
109
|
+
archived_milestone: milestone,
|
|
77
110
|
items,
|
|
78
111
|
});
|
|
79
112
|
}
|
|
@@ -93,6 +126,7 @@ function cmdAuditUat(cwd, raw) {
|
|
|
93
126
|
file_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(phaseDir, file))),
|
|
94
127
|
type: 'verification',
|
|
95
128
|
status,
|
|
129
|
+
archived_milestone: milestone,
|
|
96
130
|
items,
|
|
97
131
|
});
|
|
98
132
|
}
|
|
@@ -117,6 +151,7 @@ function cmdAuditUat(cwd, raw) {
|
|
|
117
151
|
file_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(phaseDir, deferredFile))),
|
|
118
152
|
type: 'deferred',
|
|
119
153
|
status: 'unresolved',
|
|
154
|
+
archived_milestone: milestone,
|
|
120
155
|
items,
|
|
121
156
|
});
|
|
122
157
|
}
|
|
@@ -308,6 +343,43 @@ const CHECKPOINT_FRAMES = {
|
|
|
308
343
|
banner: 'PUNTO DI CONTROLLO: Verifica richiesta',
|
|
309
344
|
instruction: 'Digita `pass` o descrivi cosa non va.',
|
|
310
345
|
},
|
|
346
|
+
dutch: {
|
|
347
|
+
banner: 'CONTROLEPUNT: Verificatie vereist',
|
|
348
|
+
instruction: 'Typ `pass` of beschrijf wat er mis is.',
|
|
349
|
+
},
|
|
350
|
+
polish: {
|
|
351
|
+
banner: 'PUNKT KONTROLNY: Wymagana weryfikacja',
|
|
352
|
+
instruction: 'Wpisz `pass` lub opisz, co jest nie tak.',
|
|
353
|
+
},
|
|
354
|
+
russian: {
|
|
355
|
+
banner: 'КОНТРОЛЬНАЯ ТОЧКА: требуется проверка',
|
|
356
|
+
instruction: 'Введите `pass` или опишите, что не так.',
|
|
357
|
+
},
|
|
358
|
+
ukrainian: {
|
|
359
|
+
banner: 'КОНТРОЛЬНА ТОЧКА: потрібна перевірка',
|
|
360
|
+
instruction: 'Введіть `pass` або опишіть, що не так.',
|
|
361
|
+
},
|
|
362
|
+
turkish: {
|
|
363
|
+
banner: 'KONTROL NOKTASI: Doğrulama gerekli',
|
|
364
|
+
instruction: '`pass` yazın veya sorunu açıklayın.',
|
|
365
|
+
},
|
|
366
|
+
hindi: {
|
|
367
|
+
banner: 'चेकपॉइंट: सत्यापन आवश्यक',
|
|
368
|
+
instruction: '`pass` लिखें या बताएं कि क्या गलत है।',
|
|
369
|
+
},
|
|
370
|
+
arabic: {
|
|
371
|
+
banner: 'نقطة تحقق: المراجعة مطلوبة',
|
|
372
|
+
instruction: 'اكتب `pass` أو صف المشكلة.',
|
|
373
|
+
direction: 'rtl',
|
|
374
|
+
},
|
|
375
|
+
vietnamese: {
|
|
376
|
+
banner: 'ĐIỂM KIỂM TRA: Cần xác minh',
|
|
377
|
+
instruction: 'Nhập `pass` hoặc mô tả vấn đề.',
|
|
378
|
+
},
|
|
379
|
+
indonesian: {
|
|
380
|
+
banner: 'TITIK PEMERIKSAAN: Verifikasi diperlukan',
|
|
381
|
+
instruction: 'Ketik `pass` atau jelaskan apa yang salah.',
|
|
382
|
+
},
|
|
311
383
|
};
|
|
312
384
|
// Free-form response_language aliases → canonical CHECKPOINT_FRAMES key.
|
|
313
385
|
const CHECKPOINT_LANGUAGE_ALIASES = {
|
|
@@ -320,21 +392,27 @@ const CHECKPOINT_LANGUAGE_ALIASES = {
|
|
|
320
392
|
chinese: 'chinese', zh: 'chinese', 'zh-cn': 'chinese', 'zh-tw': 'chinese', mandarin: 'chinese', 'simplified chinese': 'chinese', 'traditional chinese': 'chinese', '中文': 'chinese',
|
|
321
393
|
korean: 'korean', ko: 'korean', '한국어': 'korean',
|
|
322
394
|
italian: 'italian', it: 'italian', italiano: 'italian',
|
|
395
|
+
dutch: 'dutch', nl: 'dutch', nederlands: 'dutch', flemish: 'dutch', vlaams: 'dutch',
|
|
396
|
+
polish: 'polish', pl: 'polish', polski: 'polish',
|
|
397
|
+
russian: 'russian', ru: 'russian', 'ru-ru': 'russian', 'русский': 'russian',
|
|
398
|
+
ukrainian: 'ukrainian', uk: 'ukrainian', ua: 'ukrainian', 'українська': 'ukrainian',
|
|
399
|
+
turkish: 'turkish', tr: 'turkish', 'türkçe': 'turkish', turkce: 'turkish',
|
|
400
|
+
hindi: 'hindi', hi: 'hindi', 'हिन्दी': 'hindi', 'हिंदी': 'hindi',
|
|
401
|
+
arabic: 'arabic', ar: 'arabic', 'العربية': 'arabic',
|
|
402
|
+
vietnamese: 'vietnamese', vi: 'vietnamese', 'tiếng việt': 'vietnamese', 'tieng viet': 'vietnamese',
|
|
403
|
+
indonesian: 'indonesian', id: 'indonesian', 'bahasa indonesia': 'indonesian',
|
|
323
404
|
};
|
|
324
405
|
function resolveCheckpointFrame(responseLanguage) {
|
|
325
406
|
if (!responseLanguage)
|
|
326
407
|
return CHECKPOINT_FRAMES.english;
|
|
327
|
-
const key = CHECKPOINT_LANGUAGE_ALIASES[responseLanguage.trim().toLowerCase()];
|
|
408
|
+
const key = CHECKPOINT_LANGUAGE_ALIASES[responseLanguage.trim().normalize('NFC').toLowerCase()];
|
|
328
409
|
return (key && CHECKPOINT_FRAMES[key]) || CHECKPOINT_FRAMES.english;
|
|
329
410
|
}
|
|
330
|
-
// Approximate East Asian Width
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
// medium finding). Latin-script frames (English/Spanish/French/German/
|
|
336
|
-
// Portuguese/Italian) contain no wide code points, so displayWidth === length
|
|
337
|
-
// for them — no behavior change there.
|
|
411
|
+
// Approximate terminal-cell width. East Asian Width W/F code points occupy two
|
|
412
|
+
// cells, while Unicode combining marks occupy no additional cell beyond their
|
|
413
|
+
// base character. Counting only W/F ranges is insufficient for scripts such as
|
|
414
|
+
// Devanagari: Hindi vowel signs and viramas are combining marks, and treating
|
|
415
|
+
// each as a full cell visibly shifts the checkpoint box's right border.
|
|
338
416
|
function isWideCodePoint(codePoint) {
|
|
339
417
|
return ((codePoint >= 0x1100 && codePoint <= 0x115f) || // Hangul Jamo
|
|
340
418
|
codePoint === 0x2329 || codePoint === 0x232a ||
|
|
@@ -351,11 +429,17 @@ function isWideCodePoint(codePoint) {
|
|
|
351
429
|
(codePoint >= 0x20000 && codePoint <= 0x3fffd) // CJK Unified Ideographs Extension B+ / supplementary
|
|
352
430
|
);
|
|
353
431
|
}
|
|
432
|
+
// Non-spacing/enclosing marks and format controls occupy zero terminal cells.
|
|
433
|
+
// Spacing combining marks (General_Category=Mc), such as Devanagari vowel
|
|
434
|
+
// signs, still advance the cursor and must contribute one column.
|
|
435
|
+
const ZERO_WIDTH_MARK_RE = /\p{gc=Mn}|\p{gc=Me}|\p{gc=Cf}/u;
|
|
354
436
|
// Iterates by Unicode code point (not UTF-16 code unit) so astral characters
|
|
355
437
|
// are measured once, not as two surrogate units.
|
|
356
438
|
function displayWidth(text) {
|
|
357
439
|
let width = 0;
|
|
358
440
|
for (const ch of text) {
|
|
441
|
+
if (ZERO_WIDTH_MARK_RE.test(ch))
|
|
442
|
+
continue;
|
|
359
443
|
width += isWideCodePoint(ch.codePointAt(0)) ? 2 : 1;
|
|
360
444
|
}
|
|
361
445
|
return width;
|
|
@@ -370,11 +454,20 @@ function checkpointBoxLine(text) {
|
|
|
370
454
|
const padded = padLength > 0 ? content + ' '.repeat(padLength) : content;
|
|
371
455
|
return `║${padded}║`;
|
|
372
456
|
}
|
|
457
|
+
const RTL_ISOLATE = '\u2067';
|
|
458
|
+
const POP_DIRECTIONAL_ISOLATE = '\u2069';
|
|
459
|
+
function isolateCheckpointFrameText(text, frame) {
|
|
460
|
+
return frame.direction === 'rtl'
|
|
461
|
+
? `${RTL_ISOLATE}${text}${POP_DIRECTIONAL_ISOLATE}`
|
|
462
|
+
: text;
|
|
463
|
+
}
|
|
373
464
|
function buildCheckpoint(currentTest, responseLanguage) {
|
|
374
465
|
const frame = resolveCheckpointFrame(responseLanguage);
|
|
466
|
+
const banner = isolateCheckpointFrameText(frame.banner, frame);
|
|
467
|
+
const instruction = isolateCheckpointFrameText(frame.instruction, frame);
|
|
375
468
|
return [
|
|
376
469
|
'╔══════════════════════════════════════════════════════════════╗',
|
|
377
|
-
checkpointBoxLine(
|
|
470
|
+
checkpointBoxLine(banner),
|
|
378
471
|
'╚══════════════════════════════════════════════════════════════╝',
|
|
379
472
|
'',
|
|
380
473
|
`**Test ${currentTest.number}: ${currentTest.name}**`,
|
|
@@ -382,7 +475,7 @@ function buildCheckpoint(currentTest, responseLanguage) {
|
|
|
382
475
|
currentTest.expected,
|
|
383
476
|
'',
|
|
384
477
|
'──────────────────────────────────────────────────────────────',
|
|
385
|
-
|
|
478
|
+
instruction,
|
|
386
479
|
'──────────────────────────────────────────────────────────────',
|
|
387
480
|
].join('\n');
|
|
388
481
|
}
|
|
@@ -486,6 +579,165 @@ function parseGapsItems(content) {
|
|
|
486
579
|
item.reason = reason;
|
|
487
580
|
items.push(item);
|
|
488
581
|
}
|
|
582
|
+
// #2766: union with the table form. A `|`-leading line is never a `- ` bullet
|
|
583
|
+
// opener, so a section mixing bullet entries and a table surfaces both with no
|
|
584
|
+
// double-counting.
|
|
585
|
+
items.push(...parseGapsTableItems(gapsSection.body));
|
|
586
|
+
return items;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Split a section body into its GFM pipe tables, one entry per table (#2766).
|
|
590
|
+
*
|
|
591
|
+
* Shared by `parseGapsTableItems` and `parseDeferredTableItems` so the
|
|
592
|
+
* header/delimiter/table-boundary handling — the fiddly part — lives in exactly
|
|
593
|
+
* one place, and the two consumers only decide what a data row MEANS.
|
|
594
|
+
*
|
|
595
|
+
* Header detection is lookahead-free: the last data-shaped row is held in
|
|
596
|
+
* `pending` until the NEXT line decides its fate — a delimiter row
|
|
597
|
+
* (`|---|---|`) proves the held row was a header, anything else promotes it to a
|
|
598
|
+
* data row. So a conventional table drops exactly its header, a HEADERLESS table
|
|
599
|
+
* keeps every row (hand-authored planning tables often omit the delimiter), and
|
|
600
|
+
* a header with no data rows yields nothing. A prose or blank line ends the
|
|
601
|
+
* current table, so two tables separated by text are read independently and each
|
|
602
|
+
* drops its own header.
|
|
603
|
+
*
|
|
604
|
+
* Reuses the canonical `isDelimiterRow` shape check from markdown-table.cts
|
|
605
|
+
* rather than re-deriving it. Deliberately NOT routed through
|
|
606
|
+
* `parseMarkdownTable`, which reads only the FIRST table in a body and treats
|
|
607
|
+
* ragged/headerless shapes as errors (ADR-2143 §3) — correct for the mandated
|
|
608
|
+
* tables in STATE.md/ROADMAP.md, but the wrong contract here, where a malformed
|
|
609
|
+
* hand-written table must still surface its rows rather than be dropped.
|
|
610
|
+
*/
|
|
611
|
+
function collectTableRows(sectionBody) {
|
|
612
|
+
const tables = [];
|
|
613
|
+
let current = null;
|
|
614
|
+
let pending = null;
|
|
615
|
+
const ensure = () => {
|
|
616
|
+
if (!current)
|
|
617
|
+
current = { header: null, rows: [] };
|
|
618
|
+
};
|
|
619
|
+
const flushPending = () => {
|
|
620
|
+
if (pending) {
|
|
621
|
+
ensure();
|
|
622
|
+
current.rows.push(pending);
|
|
623
|
+
pending = null;
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
const endTable = () => {
|
|
627
|
+
flushPending();
|
|
628
|
+
if (current) {
|
|
629
|
+
tables.push(current);
|
|
630
|
+
current = null;
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
for (const rawLine of sectionBody.split('\n')) {
|
|
634
|
+
const line = rawLine.replace(/\r$/, '').trim();
|
|
635
|
+
if (!line.startsWith('|')) {
|
|
636
|
+
endTable();
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const cells = splitTableRow(line);
|
|
640
|
+
if (cells.length === 0)
|
|
641
|
+
continue;
|
|
642
|
+
if (isDelimiterRow(cells)) {
|
|
643
|
+
ensure();
|
|
644
|
+
current.header = pending; // may be null for a delimiter-first table
|
|
645
|
+
pending = null;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
flushPending();
|
|
649
|
+
pending = cells;
|
|
650
|
+
}
|
|
651
|
+
endTable();
|
|
652
|
+
return tables;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Header-name → canonical Gaps field (#2766).
|
|
656
|
+
*
|
|
657
|
+
* Anchored on the `## Gaps` field vocabulary `templates/UAT.md` mandates for the
|
|
658
|
+
* YAML-lite bullet form (truth/status/reason/severity/test), plus the obvious
|
|
659
|
+
* synonyms a human writing the same information as a table reaches for instead.
|
|
660
|
+
*/
|
|
661
|
+
const GAPS_COLUMN_ALIASES = {
|
|
662
|
+
truth: 'truth', gap: 'truth', finding: 'truth', item: 'truth',
|
|
663
|
+
description: 'truth', issue: 'truth', name: 'truth',
|
|
664
|
+
status: 'status', result: 'status', state: 'status',
|
|
665
|
+
reason: 'reason', note: 'reason', notes: 'reason',
|
|
666
|
+
detail: 'reason', details: 'reason', evidence: 'reason',
|
|
667
|
+
severity: 'severity',
|
|
668
|
+
test: 'test', '#': 'test', 'test #': 'test', 'test number': 'test',
|
|
669
|
+
};
|
|
670
|
+
function mapGapsHeader(header) {
|
|
671
|
+
if (!header)
|
|
672
|
+
return null;
|
|
673
|
+
const columns = {};
|
|
674
|
+
header.forEach((cell, idx) => {
|
|
675
|
+
const key = GAPS_COLUMN_ALIASES[cell.trim().toLowerCase().replace(/\*+/g, '')];
|
|
676
|
+
if (key && !(key in columns))
|
|
677
|
+
columns[key] = idx;
|
|
678
|
+
});
|
|
679
|
+
return Object.keys(columns).length > 0 ? columns : null;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Extract gap entries from GFM pipe tables in a `## Gaps` section (#2766) — a
|
|
683
|
+
* UNION with the YAML-lite bullet scan in `parseGapsItems`, for the same reason
|
|
684
|
+
* `parseDeferredTableItems` exists: `splitGapsEntries` keys entirely on `- `
|
|
685
|
+
* bullet openers, so a table-shaped `## Gaps` section yielded ZERO items and
|
|
686
|
+
* every finding in it was silently invisible.
|
|
687
|
+
*
|
|
688
|
+
* Neither `templates/UAT.md` nor `templates/verification-report.md` documents a
|
|
689
|
+
* table for this section (both mandate the bullet/numbered form), so a table
|
|
690
|
+
* here is off-template hand-authoring — which is precisely why it must not fail
|
|
691
|
+
* silently. Note `parseVerificationItems` in this same file already reads table
|
|
692
|
+
* rows AND numbered AND bullet items as a union because the live sections mix
|
|
693
|
+
* shapes; the Gaps and deferred parsers never got the same treatment.
|
|
694
|
+
*
|
|
695
|
+
* When a header row is present its columns are mapped by name against the
|
|
696
|
+
* template's own field vocabulary (see GAPS_COLUMN_ALIASES) so a tabled gap
|
|
697
|
+
* carries the same status/reason/test fields as its bullet equivalent and
|
|
698
|
+
* `categorizeItem` classifies it identically. With no recognizable header, the
|
|
699
|
+
* row degrades to a joined-cells name with status `unknown` — surfaced, not
|
|
700
|
+
* dropped, matching this module's established fail-safe stance.
|
|
701
|
+
*
|
|
702
|
+
* Resolution follows the bullet path exactly: an entry is skipped ONLY on an
|
|
703
|
+
* explicit resolved marker — the mapped `status` column reading `resolved`, or,
|
|
704
|
+
* absent a status column, any cell reading exactly `resolved`. A gap with no
|
|
705
|
+
* parseable status is NEVER treated as resolved.
|
|
706
|
+
*/
|
|
707
|
+
function parseGapsTableItems(sectionBody) {
|
|
708
|
+
const items = [];
|
|
709
|
+
for (const { header, rows } of collectTableRows(sectionBody)) {
|
|
710
|
+
const columns = mapGapsHeader(header);
|
|
711
|
+
for (const cells of rows) {
|
|
712
|
+
const at = (key) => (columns && key in columns ? (cells[columns[key]] ?? '').trim() : '');
|
|
713
|
+
const rawStatus = at('status');
|
|
714
|
+
if (rawStatus && rawStatus.toLowerCase() === 'resolved')
|
|
715
|
+
continue;
|
|
716
|
+
// No status column: fall back to an explicit resolved marker in any cell
|
|
717
|
+
// (the headerless-table equivalent of `status: resolved`).
|
|
718
|
+
if (!columns || !('status' in columns)) {
|
|
719
|
+
if (cells.some(c => /^resolved$/i.test(c.trim())))
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const truth = at('truth');
|
|
723
|
+
const reason = at('reason');
|
|
724
|
+
const testNum = at('test');
|
|
725
|
+
const name = truth || cells.filter(c => c !== '').join(' — ');
|
|
726
|
+
if (!name)
|
|
727
|
+
continue;
|
|
728
|
+
const status = rawStatus || 'unknown';
|
|
729
|
+
const item = {
|
|
730
|
+
name,
|
|
731
|
+
result: status,
|
|
732
|
+
category: categorizeItem(status, reason || undefined, undefined),
|
|
733
|
+
};
|
|
734
|
+
if (testNum && /^\d+$/.test(testNum))
|
|
735
|
+
item.test = parseInt(testNum, 10);
|
|
736
|
+
if (reason)
|
|
737
|
+
item.reason = reason;
|
|
738
|
+
items.push(item);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
489
741
|
return items;
|
|
490
742
|
}
|
|
491
743
|
// ─── parseDeferredItems ────────────────────────────────────────────────────────
|
|
@@ -533,6 +785,49 @@ function parseDeferredItems(content) {
|
|
|
533
785
|
category: 'deferred',
|
|
534
786
|
});
|
|
535
787
|
}
|
|
788
|
+
// #2766: union with the table form — see parseDeferredTableItems. Executors
|
|
789
|
+
// write this file by hand with no mandated shape, and a GFM table is a natural
|
|
790
|
+
// choice for the common "test → failing seeds" case, which produced ZERO items.
|
|
791
|
+
items.push(...parseDeferredTableItems(sectionBody));
|
|
792
|
+
return items;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Extract deferred entries from GFM pipe tables in a deferred-items.md body
|
|
796
|
+
* (#2766) — a UNION with the bullet scan in `parseDeferredItems`.
|
|
797
|
+
*
|
|
798
|
+
* Cells are joined with ` — ` rather than taking only the first: these tables
|
|
799
|
+
* carry the useful detail in the later columns (the failing seeds, the reason,
|
|
800
|
+
* the owner), and dropping them would surface a name with no context.
|
|
801
|
+
*
|
|
802
|
+
* A row is skipped when any cell reads exactly `resolved`/`done`/`pass`
|
|
803
|
+
* (case-insensitive), mirroring the "explicit resolution only" convention
|
|
804
|
+
* `parseGapsItems` uses for `status: resolved` and `parseVerificationItems` uses
|
|
805
|
+
* for its `hasPassResult` cell scan — so a human can close a tabled deferred
|
|
806
|
+
* item in place and keep deferred-items.md the single source of truth.
|
|
807
|
+
*
|
|
808
|
+
* Deliberately permissive: an unrelated table in a deferred-items.md (say a
|
|
809
|
+
* table of environment notes) will surface as deferred entries. That is the
|
|
810
|
+
* correct fail-safe direction for a false-NEGATIVE bug — the whole file exists to
|
|
811
|
+
* record outstanding work, and this module's established stance (see
|
|
812
|
+
* parseGapsItems' 'unknown'-status fallback) is to surface a questionable entry
|
|
813
|
+
* rather than silently drop a real one.
|
|
814
|
+
*/
|
|
815
|
+
function parseDeferredTableItems(sectionBody) {
|
|
816
|
+
const items = [];
|
|
817
|
+
for (const { rows } of collectTableRows(sectionBody)) {
|
|
818
|
+
for (const cells of rows) {
|
|
819
|
+
if (cells.some(c => /^(resolved|done|pass)$/i.test(c)))
|
|
820
|
+
continue;
|
|
821
|
+
const name = cells.filter(c => c !== '').join(' — ');
|
|
822
|
+
if (!name)
|
|
823
|
+
continue;
|
|
824
|
+
items.push({
|
|
825
|
+
name,
|
|
826
|
+
result: 'unresolved',
|
|
827
|
+
category: 'deferred',
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
}
|
|
536
831
|
return items;
|
|
537
832
|
}
|
|
538
833
|
/**
|
|
@@ -834,5 +1129,9 @@ module.exports = {
|
|
|
834
1129
|
cmdRenderCheckpoint,
|
|
835
1130
|
parseCurrentTest,
|
|
836
1131
|
buildCheckpoint,
|
|
1132
|
+
CHECKPOINT_FRAMES,
|
|
1133
|
+
CHECKPOINT_LANGUAGE_ALIASES,
|
|
1134
|
+
resolveCheckpointFrame,
|
|
1135
|
+
checkpointBoxLine,
|
|
837
1136
|
parseDeferredItems,
|
|
838
1137
|
};
|
|
@@ -50,11 +50,20 @@ const UNUSABLE_REASON = Object.freeze({
|
|
|
50
50
|
* simply has no ROADMAP yet: absence returns the same sentinel, silently. (#1881)
|
|
51
51
|
*/
|
|
52
52
|
ROADMAP_UNREADABLE: 'roadmap_unreadable',
|
|
53
|
+
/**
|
|
54
|
+
* A `last_activity` value in STATE.md frontmatter is present but unparseable as a date.
|
|
55
|
+
* Distinct from absent: absence means the field was never set, while an unparseable value
|
|
56
|
+
* means it was set to something Date.parse rejects. Per ADR-1411 amendment: corrupt is not
|
|
57
|
+
* absent — the fallback (stale_activity: false) stays, but a diagnostic is emitted so the
|
|
58
|
+
* silent degradation is visible. (#3099, sixth #1879 site)
|
|
59
|
+
*/
|
|
60
|
+
LAST_ACTIVITY_UNPARSEABLE: 'last_activity_unparseable',
|
|
53
61
|
});
|
|
54
62
|
/** One human-readable clause per reason. Prose lives here, never in a test assertion. */
|
|
55
63
|
const REASON_PROSE = Object.freeze({
|
|
56
64
|
[UNUSABLE_REASON.FRONTMATTER_UNTERMINATED]: 'frontmatter opens with "---" but never closes; metadata was NOT applied',
|
|
57
65
|
[UNUSABLE_REASON.ROADMAP_UNREADABLE]: 'ROADMAP.md exists but could not be read; phase and milestone lookups fell back to defaults',
|
|
66
|
+
[UNUSABLE_REASON.LAST_ACTIVITY_UNPARSEABLE]: 'last_activity in STATE.md is present but unparseable as a date; stale_activity fell back to false (idle-stranded suppressed)',
|
|
58
67
|
});
|
|
59
68
|
// ─── Dedup state ──────────────────────────────────────────────────────────────
|
|
60
69
|
/**
|
|
@@ -213,23 +213,26 @@ function missingResult(runtime, phaseArg) {
|
|
|
213
213
|
}
|
|
214
214
|
function findStaleVerificationSummary(phaseDir, fsImpl = node_fs_1.default, phaseCleanCommitTimesMs = defaultPhaseCleanCommitTimesMs) {
|
|
215
215
|
// FS errors (TOCTOU: a SUMMARY listed by scanPhasePlans then removed before statSync;
|
|
216
|
-
// unreadable dir; broken symlink; file->dir swap) must degrade
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
// (Review B1 on #1548.)
|
|
216
|
+
// unreadable dir; broken symlink; file->dir swap) must degrade rather than throw
|
|
217
|
+
// uncaught into callers that are NOT under the planning lock (init.manager /
|
|
218
|
+
// init.progress / uat-predicate). Mirrors readVerificationStatus's no-throw
|
|
219
|
+
// contract; `fsImpl` threads the same injectable-fs seam for parity/testing.
|
|
220
|
+
// (Review B1 on #1548.) The degraded result is `{determined:false}`, NOT the
|
|
221
|
+
// same value as a completed "nothing is stale" check — see StaleCheckResult
|
|
222
|
+
// doc and #3057 B3. The caller decides how to route an indeterminate result;
|
|
223
|
+
// this function only reports what it actually knows.
|
|
221
224
|
try {
|
|
222
225
|
const phaseFiles = fsImpl.readdirSync(phaseDir);
|
|
223
226
|
const verificationFile = phaseFiles.filter((f) => f.endsWith('-VERIFICATION.md')).sort()[0];
|
|
224
227
|
if (!verificationFile)
|
|
225
|
-
return
|
|
228
|
+
return { determined: true, stale: false };
|
|
226
229
|
const summaryFiles = scanPhasePlans(phaseDir).summaryFiles
|
|
227
230
|
.slice()
|
|
228
231
|
.sort();
|
|
229
232
|
// No summary can be newer than the verification → never stale. Return before
|
|
230
233
|
// touching git so a phase with no summaries costs zero subprocesses. (#2348)
|
|
231
234
|
if (summaryFiles.length === 0)
|
|
232
|
-
return
|
|
235
|
+
return { determined: true, stale: false };
|
|
233
236
|
// Each file's effective "last changed" time = its commit time when committed
|
|
234
237
|
// AND clean (content-tied and clone-stable), else its filesystem mtime (the
|
|
235
238
|
// uncommitted working-tree edit). Both are real wall-clock change times, so
|
|
@@ -244,13 +247,13 @@ function findStaleVerificationSummary(phaseDir, fsImpl = node_fs_1.default, phas
|
|
|
244
247
|
// The caller only needs whether the phase is stale, not which summary —
|
|
245
248
|
// the first stale summary (in sorted order) is enough. Short-circuit.
|
|
246
249
|
if (effectiveTimeMs(summaryFile) > verificationTimeMs) {
|
|
247
|
-
return { verificationFile, summaryFile };
|
|
250
|
+
return { determined: true, stale: true, verificationFile, summaryFile };
|
|
248
251
|
}
|
|
249
252
|
}
|
|
250
|
-
return
|
|
253
|
+
return { determined: true, stale: false };
|
|
251
254
|
}
|
|
252
255
|
catch {
|
|
253
|
-
return
|
|
256
|
+
return { determined: false };
|
|
254
257
|
}
|
|
255
258
|
}
|
|
256
259
|
/**
|
|
@@ -265,6 +268,12 @@ function findStaleVerificationSummary(phaseDir, fsImpl = node_fs_1.default, phas
|
|
|
265
268
|
* If no frontmatter block or no `status` key → status 'missing'.
|
|
266
269
|
* 3. Map to routing table. Unknown non-empty value → status 'unknown'.
|
|
267
270
|
*
|
|
271
|
+
* The internal staleness check can itself fail (fs / scanPhasePlans / clock
|
|
272
|
+
* error); when it does, `status` is routed as if nothing were stale (the
|
|
273
|
+
* pre-existing no-throw fail-open contract — unchanged), but the returned
|
|
274
|
+
* result carries `staleCheckIndeterminate: true` so a caller can distinguish
|
|
275
|
+
* "checked; nothing is stale" from "could not check" (#3057 B3).
|
|
276
|
+
*
|
|
268
277
|
* @param phaseDir - Absolute path to the phase directory.
|
|
269
278
|
* @param opts - Options. `opts.fs` allows test injection (defaults to node:fs).
|
|
270
279
|
* `opts.runtime` selects the command surface `next_command` is
|
|
@@ -331,8 +340,8 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
331
340
|
next_command: projectNextCommand('plan-phase', runtime, `${phaseArg} --gaps`),
|
|
332
341
|
};
|
|
333
342
|
}
|
|
334
|
-
const
|
|
335
|
-
if (
|
|
343
|
+
const staleCheck = findStaleVerificationSummary(phaseDir, fsImpl, phaseCleanCommitTimesMs);
|
|
344
|
+
if (staleCheck.determined && staleCheck.stale) {
|
|
336
345
|
const entry = VERIFICATION_ROUTING_TABLE['stale'];
|
|
337
346
|
return {
|
|
338
347
|
status: entry.status,
|
|
@@ -340,6 +349,12 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
340
349
|
next_command: projectNextCommand('verify-work', runtime, phaseArg),
|
|
341
350
|
};
|
|
342
351
|
}
|
|
352
|
+
// staleCheck is either {determined:true, stale:false} (checked; nothing
|
|
353
|
+
// stale) or {determined:false} (could not check — fs/scan/clock failure).
|
|
354
|
+
// Both fall through to normal routing below (the pre-existing no-throw
|
|
355
|
+
// fail-open contract is unchanged), but the indeterminate case is flagged
|
|
356
|
+
// on the returned result so a caller can tell the two apart (#3057 B3).
|
|
357
|
+
const staleCheckIndeterminate = !staleCheck.determined;
|
|
343
358
|
// 3. Route — exclude internal sentinels from raw-file lookup (they are
|
|
344
359
|
// constructed internally above, never written by the verifier).
|
|
345
360
|
if (rawStatus in VERIFICATION_ROUTING_TABLE &&
|
|
@@ -352,6 +367,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
352
367
|
status: entry.status,
|
|
353
368
|
next_action: entry.next_action,
|
|
354
369
|
next_command: projectNextCommand(entry.next_command, runtime, phaseArg),
|
|
370
|
+
...(staleCheckIndeterminate ? { staleCheckIndeterminate: true } : {}),
|
|
355
371
|
};
|
|
356
372
|
}
|
|
357
373
|
// Unknown value
|
|
@@ -360,6 +376,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
360
376
|
status: unknownRoute.status,
|
|
361
377
|
next_action: `Unexpected verification status '${rawStatus}'. Re-run execute-phase verification.`,
|
|
362
378
|
next_command: projectNextCommand(unknownRoute.next_command, runtime, phaseArg),
|
|
379
|
+
...(staleCheckIndeterminate ? { staleCheckIndeterminate: true } : {}),
|
|
363
380
|
};
|
|
364
381
|
}
|
|
365
382
|
/**
|
|
@@ -161,6 +161,15 @@ function verifySummaryCore(cwd, summaryPath, checkFileCount, opts) {
|
|
|
161
161
|
// is still not read. Recovering it needs a real frontmatter parse, which is
|
|
162
162
|
// deliberately left as a follow-up rather than smuggled in here.
|
|
163
163
|
const mentionedFiles = new Set();
|
|
164
|
+
// #2844: Pattern 1 matches any backticked path-like token. A SUMMARY body is
|
|
165
|
+
// predominantly about what the phase DID, so a backticked path in prose ("Built
|
|
166
|
+
// `src/kept.ts`", a `- \`src/x.ts\`` list item) is a legitimate claim (#2685
|
|
167
|
+
// pins this). The false-positive class #2844 fixes is a path mentioned as a
|
|
168
|
+
// FUTURE/CONDITIONAL deliverable — "next phase will add `shared/types.ts`",
|
|
169
|
+
// "planned", "would", "to be created" — which is NOT a claim about this phase.
|
|
170
|
+
// Exclude those lines rather than requiring an explicit claim verb (which would
|
|
171
|
+
// drop the legitimate "Built …" / list-item forms #2685 protects).
|
|
172
|
+
const isFutureMention = (line) => /\b(?:will(?:\s+(?:add|create|build|land))?(?:[^.])?|(?:next|later|future)\s+phase|planned?|would\s+(?:be|add|create|build)|to\s+be\s+(?:added|created|built)|eventually|not\s+yet)\b/i.test(line);
|
|
164
173
|
const patterns = [
|
|
165
174
|
/`([^`]+\.[a-zA-Z]+)`/g,
|
|
166
175
|
/(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`[\]]+\.[a-zA-Z]+)`?/gi,
|
|
@@ -169,9 +178,16 @@ function verifySummaryCore(cwd, summaryPath, checkFileCount, opts) {
|
|
|
169
178
|
let m;
|
|
170
179
|
while ((m = pattern.exec(content)) !== null) {
|
|
171
180
|
const filePath = m[1];
|
|
172
|
-
if (filePath
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
if (!filePath || !isProbableProjectFile(filePath))
|
|
182
|
+
continue;
|
|
183
|
+
// #2844: skip a backticked path on a future/conditional line — it names a
|
|
184
|
+
// deliverable this phase did NOT produce, so probing it is a false positive.
|
|
185
|
+
const lineStart = content.lastIndexOf('\n', m.index) + 1;
|
|
186
|
+
const lineEnd = content.indexOf('\n', m.index);
|
|
187
|
+
const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
|
|
188
|
+
if (isFutureMention(line))
|
|
189
|
+
continue;
|
|
190
|
+
mentionedFiles.add(filePath);
|
|
175
191
|
}
|
|
176
192
|
}
|
|
177
193
|
const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
|
|
@@ -1596,7 +1612,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1596
1612
|
}
|
|
1597
1613
|
}
|
|
1598
1614
|
try {
|
|
1599
|
-
const agentStatus = checkAgentsInstalled();
|
|
1615
|
+
const agentStatus = checkAgentsInstalled(_slashRuntime, cwd);
|
|
1600
1616
|
if (!agentStatus.agents_installed) {
|
|
1601
1617
|
if ((agentStatus.installed_agents).length === 0) {
|
|
1602
1618
|
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`);
|
|
@@ -1719,6 +1735,14 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1719
1735
|
if (isActiveWorktree)
|
|
1720
1736
|
continue;
|
|
1721
1737
|
addIssue('warning', 'W017', `Stale git worktree: ${worktreePath} (last modified ${finding['ageMinutes']} minutes ago)`, `Run: git worktree remove ${worktreePath} --force`);
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
// #3050/#3057 (B5): a 'unverified' finding means existsSync confirmed
|
|
1741
|
+
// the worktree is present but statSync threw, so orphan/stale status
|
|
1742
|
+
// could not be determined for THIS entry — it must not be silently
|
|
1743
|
+
// dropped (that would be the exact fail-open the row exists to close).
|
|
1744
|
+
if (finding['kind'] === 'unverified') {
|
|
1745
|
+
addIssue('warning', 'W020', `Worktree health check degraded: could not stat ${finding['path']} — presence/staleness could not be verified`, 'Check filesystem permissions on the worktree path, or investigate why statSync failed for it');
|
|
1722
1746
|
}
|
|
1723
1747
|
}
|
|
1724
1748
|
}
|
|
@@ -2023,7 +2047,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
2023
2047
|
return result;
|
|
2024
2048
|
}
|
|
2025
2049
|
function cmdValidateAgents(cwd, raw) {
|
|
2026
|
-
const agentStatus = checkAgentsInstalled();
|
|
2050
|
+
const agentStatus = checkAgentsInstalled((0, runtime_slash_cjs_1.resolveRuntime)(cwd), cwd);
|
|
2027
2051
|
const expected = Object.keys(MODEL_PROFILES);
|
|
2028
2052
|
output({
|
|
2029
2053
|
agents_dir: agentStatus.agents_dir,
|