@planu/cli 5.7.5 → 5.7.7
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/CHANGELOG.md +88 -0
- package/README.md +3 -3
- package/dist/.planu-build.json +1 -1
- package/dist/cli/commands/package-handoff.js +30 -5
- package/dist/config/compliance-profiles.json +36 -3
- package/dist/config/environment-schema.json +21 -0
- package/dist/config/hook-templates/planu-review-panel-trigger.sh +2 -3
- package/dist/config/hook-templates/planu-session-safeguard.sh +22 -15
- package/dist/config/official-sdd-tools.d.ts +1 -1
- package/dist/config/official-sdd-tools.js +11 -24
- package/dist/config/registries/hosts/codex.json +14 -30
- package/dist/config/registries/hosts/opencode.json +1 -6
- package/dist/config/server-instructions.js +0 -21
- package/dist/config/skill-templates/planu-context-assets.md +3 -6
- package/dist/config/skill-templates/planu-implement.md +2 -2
- package/dist/config/skill-templates/planu-multi-teammate-review.md +8 -12
- package/dist/config/skill-templates/planu-release.md +2 -7
- package/dist/config/skill-templates/planu-validate.md +3 -3
- package/dist/config/spec-templates/crud-rest-api/template.json +5 -1
- package/dist/config/spec-templates/file-upload-s3/template.json +5 -1
- package/dist/config/spec-templates/stripe-payments/template.json +5 -1
- package/dist/config/spec-templates/webhook-system/template.json +5 -1
- package/dist/config/subagent-templates/planu-challenger.md +4 -9
- package/dist/config/subagent-templates/planu-spec-implementer.md +2 -4
- package/dist/config/subagent-templates/planu-validator.md +5 -8
- package/dist/config/tool-groups.json +6 -144
- package/dist/engine/ambiguity-scorer.js +40 -1
- package/dist/engine/autopilot/bootstrap.js +1 -1
- package/dist/engine/code-impact-analyzer.js +2 -6
- package/dist/engine/context-orchestrator/index.js +2 -2
- package/dist/engine/convention-scanner/codebase-scanner.js +2 -2
- package/dist/engine/dogfooding/runtime-gap-detector.js +2 -2
- package/dist/engine/evidence-gates/artifact-reader.d.ts +1 -0
- package/dist/engine/evidence-gates/artifact-reader.js +1 -1
- package/dist/engine/evidence-gates/evidence-autofill.d.ts +13 -5
- package/dist/engine/evidence-gates/evidence-autofill.js +106 -43
- package/dist/engine/evidence-gates/evidence-skeletons.js +3 -1
- package/dist/engine/evidence-gates/lifecycle-gate.js +7 -4
- package/dist/engine/evidence-index/index-builder.js +21 -4
- package/dist/engine/execution/job-runtime.js +1 -1
- package/dist/engine/execution/package-handoff-job-executor.d.ts +22 -0
- package/dist/engine/execution/package-handoff-job-executor.js +490 -0
- package/dist/engine/execution/validate-job-executor.js +20 -8
- package/dist/engine/git/exec-git.d.ts +4 -0
- package/dist/engine/git/exec-git.js +14 -0
- package/dist/engine/handoff-artifacts/implementation-review-reader.js +15 -2
- package/dist/engine/handoff-artifacts/schemas.d.ts +2 -0
- package/dist/engine/handoff-artifacts/schemas.js +1 -0
- package/dist/engine/handoff-format.d.ts +36 -0
- package/dist/engine/handoff-format.js +314 -0
- package/dist/engine/handoff-packager.d.ts +5 -3
- package/dist/engine/handoff-packager.js +89 -32
- package/dist/engine/host-tool-filter.js +20 -49
- package/dist/engine/human-summary.js +14 -3
- package/dist/engine/implementation-contract/common.d.ts +8 -3
- package/dist/engine/implementation-contract/common.js +9 -6
- package/dist/engine/implementation-contract/evaluator.js +4 -6
- package/dist/engine/implementation-contract/renderer.js +18 -23
- package/dist/engine/project-health-checker.js +2 -2
- package/dist/engine/readiness-checker.js +43 -5
- package/dist/engine/reconcile/apply-changes.js +4 -0
- package/dist/engine/reconcile/propagate-mirrors.d.ts +3 -0
- package/dist/engine/reconcile/propagate-mirrors.js +311 -0
- package/dist/engine/self-healing/healer.js +12 -5
- package/dist/engine/self-healing/strategies/lint-fix.d.ts +1 -1
- package/dist/engine/self-healing/strategies/lint-fix.js +4 -1
- package/dist/engine/self-healing/strategies/test-fix.d.ts +4 -3
- package/dist/engine/self-healing/strategies/test-fix.js +32 -14
- package/dist/engine/self-healing/strategies/typescript-fix.d.ts +1 -1
- package/dist/engine/self-healing/strategies/typescript-fix.js +28 -10
- package/dist/engine/session/checkpoint-writer.js +8 -4
- package/dist/engine/session/session-tracker.d.ts +5 -16
- package/dist/engine/session/session-tracker.js +29 -26
- package/dist/engine/session-state/writer.js +3 -11
- package/dist/engine/skill-generator/workflow-skill-generator.d.ts +4 -2
- package/dist/engine/skill-generator/workflow-skill-generator.js +113 -38
- package/dist/engine/spec-format/bdd-parser.d.ts +1 -11
- package/dist/engine/spec-format/bdd-parser.js +40 -11
- package/dist/engine/spec-format/lean-spec-generator.js +32 -12
- package/dist/engine/spec-format/retired-scaffold.d.ts +3 -0
- package/dist/engine/spec-format/retired-scaffold.js +5 -0
- package/dist/engine/spec-format/technical-md-populator.d.ts +1 -0
- package/dist/engine/spec-format/technical-md-populator.js +27 -0
- package/dist/engine/spec-format/unified-spec-builder.d.ts +1 -0
- package/dist/engine/spec-format/unified-spec-builder.js +30 -20
- package/dist/engine/spec-grounding/contract.d.ts +3 -3
- package/dist/engine/spec-grounding/contract.js +65 -4
- package/dist/engine/spec-grounding/grounding-entry-parser.d.ts +5 -0
- package/dist/engine/spec-grounding/grounding-entry-parser.js +185 -0
- package/dist/engine/spec-quality/generic-output-gate.js +37 -3
- package/dist/engine/technical-enricher/index.js +4 -1
- package/dist/engine/type-safety-gate.js +3 -11
- package/dist/engine/universal-rules/catalog.js +0 -2
- package/dist/engine/universal-rules/rules/planu-workflow.js +1 -1
- package/dist/engine/validation/durable-validation.d.ts +3 -2
- package/dist/engine/validation/durable-validation.js +106 -40
- package/dist/engine/validation/validation-freshness.d.ts +3 -2
- package/dist/engine/validation/validation-freshness.js +22 -16
- package/dist/engine/validation/validation-worktree.js +34 -0
- package/dist/hosts/claude-code/ux/mcp-prompts.js +8 -8
- package/dist/index.js +3 -17
- package/dist/resources/process.js +38 -65
- package/dist/storage/session-state-store.js +2 -0
- package/dist/storage/spec-store.js +8 -1
- package/dist/tools/bump-spec-version.js +5 -5
- package/dist/tools/challenge-spec/scenarios-utils.js +2 -2
- package/dist/tools/configure-checkpoint-policy.js +3 -3
- package/dist/tools/create-spec-helpers.js +2 -5
- package/dist/tools/create-spec.js +21 -4
- package/dist/tools/facilitate.js +3 -6
- package/dist/tools/git/branch-ops.js +16 -4
- package/dist/tools/github-release-handler.js +1 -20
- package/dist/tools/init-project/agents-md-writer.js +6 -6
- package/dist/tools/init-project/claude-md-generator.d.ts +0 -1
- package/dist/tools/init-project/claude-md-generator.js +0 -35
- package/dist/tools/init-project/handler.js +7 -5
- package/dist/tools/init-project/per-client-files-writer.js +5 -5
- package/dist/tools/init-project/planu-workflow-generator.js +2 -13
- package/dist/tools/init-project/portable-index-reconciler.d.ts +2 -1
- package/dist/tools/init-project/portable-index-reconciler.js +7 -2
- package/dist/tools/init-project/rules-generator.d.ts +0 -11
- package/dist/tools/init-project/rules-generator.js +4 -85
- package/dist/tools/init-project/scaffold-writer.d.ts +0 -1
- package/dist/tools/init-project/scaffold-writer.js +1 -12
- package/dist/tools/init-project/skills-multi-teammate-review-writer.js +1 -1
- package/dist/tools/jobs/handlers.d.ts +0 -6
- package/dist/tools/jobs/handlers.js +13 -40
- package/dist/tools/list-specs.js +6 -21
- package/dist/tools/multi-teammate-review.js +1 -14
- package/dist/tools/package-handoff.d.ts +3 -2
- package/dist/tools/package-handoff.js +153 -427
- package/dist/tools/reconcile-session-safeguard-hook.js +22 -15
- package/dist/tools/register-platform-tools/design-stack-tools.js +3 -3
- package/dist/tools/register-sdd-tools.js +0 -23
- package/dist/tools/register-spec-tools/analysis-tools.js +22 -9
- package/dist/tools/register-spec-tools/core-spec-tools.js +115 -70
- package/dist/tools/schemas/index.d.ts +1 -1
- package/dist/tools/schemas/index.js +1 -1
- package/dist/tools/schemas/output-schemas.d.ts +1 -18
- package/dist/tools/schemas/output-schemas.js +1 -11
- package/dist/tools/schemas/package-handoff-output-schema.d.ts +40 -0
- package/dist/tools/schemas/package-handoff-output-schema.js +68 -0
- package/dist/tools/schemas/validate-output-schema.d.ts +1 -2
- package/dist/tools/schemas/validate-output-schema.js +3 -2
- package/dist/tools/session-checkpoint.js +2 -2
- package/dist/tools/skill-registry/index.d.ts +0 -1
- package/dist/tools/skill-registry/index.js +0 -1
- package/dist/tools/sync-spec-state-handler.d.ts +2 -2
- package/dist/tools/sync-spec-state-handler.js +23 -3
- package/dist/tools/tool-registry/core-tools.js +72 -164
- package/dist/tools/tool-registry/group-infra.js +2 -39
- package/dist/tools/tool-registry/group-quality-compliance.js +20 -15
- package/dist/tools/tool-registry-helpers.js +6 -2
- package/dist/tools/update-status/batch.js +17 -0
- package/dist/tools/update-status/dod-gates.js +14 -1
- package/dist/tools/update-status/done-receipt-verifier.d.ts +22 -0
- package/dist/tools/update-status/done-receipt-verifier.js +56 -26
- package/dist/tools/update-status/evidence-gate.js +10 -21
- package/dist/tools/update-status/file-sync.d.ts +1 -0
- package/dist/tools/update-status/file-sync.js +6 -0
- package/dist/tools/update-status/index.d.ts +6 -0
- package/dist/tools/update-status/index.js +39 -44
- package/dist/tools/update-status-actions.js +2 -7
- package/dist/tools/validate.js +34 -28
- package/dist/transports/oauth-validator.js +12 -1
- package/dist/transports/transport-factory.d.ts +2 -1
- package/dist/transports/transport-factory.js +19 -0
- package/dist/types/common/primitives.d.ts +2 -0
- package/dist/types/durable-job.d.ts +45 -0
- package/dist/types/durable-validation.d.ts +1 -1
- package/dist/types/evidence-autofill.d.ts +13 -1
- package/dist/types/handoff-artifacts.d.ts +2 -1
- package/dist/types/readiness.d.ts +8 -1
- package/dist/types/reconcile.d.ts +6 -0
- package/dist/types/skill-registry.d.ts +26 -1
- package/dist/types/spec/core.d.ts +5 -0
- package/dist/types/spec/inputs.d.ts +2 -2
- package/dist/types/spec-format.d.ts +11 -1
- package/dist/types/spec-grounding.d.ts +26 -0
- package/dist/types/transport.d.ts +1 -0
- package/dist/types/validation-receipt.d.ts +27 -0
- package/package.json +3 -2
- package/planu-plugin.json +13 -26
- package/src/i18n/messages/en.json +1 -1
- package/src/i18n/messages/es.json +1 -1
- package/src/i18n/messages/pt.json +1 -1
- package/dist/engine/skill-generator/conventions-hasher.d.ts +0 -7
- package/dist/engine/skill-generator/conventions-hasher.js +0 -24
- package/dist/engine/universal-rules/rules/agent-teams.d.ts +0 -3
- package/dist/engine/universal-rules/rules/agent-teams.js +0 -63
- package/dist/tools/clarify-requirements/multiple-choice.d.ts +0 -30
- package/dist/tools/clarify-requirements/multiple-choice.js +0 -306
- package/dist/tools/clarify-requirements/questions-context.d.ts +0 -9
- package/dist/tools/clarify-requirements/questions-context.js +0 -74
- package/dist/tools/clarify-requirements/questions.d.ts +0 -16
- package/dist/tools/clarify-requirements/questions.js +0 -96
- package/dist/tools/clarify-requirements.d.ts +0 -4
- package/dist/tools/clarify-requirements.js +0 -314
- package/dist/tools/code-graph-handler.d.ts +0 -6
- package/dist/tools/code-graph-handler.js +0 -72
- package/dist/tools/create-rule.d.ts +0 -4
- package/dist/tools/create-rule.js +0 -107
- package/dist/tools/feedback-handler.d.ts +0 -7
- package/dist/tools/feedback-handler.js +0 -150
- package/dist/tools/semantic-search-handler.d.ts +0 -7
- package/dist/tools/semantic-search-handler.js +0 -71
- package/dist/tools/skill-registry/search.d.ts +0 -11
- package/dist/tools/skill-registry/search.js +0 -159
- package/dist/tools/tool-registry/deprecated-stubs.d.ts +0 -19
- package/dist/tools/tool-registry/deprecated-stubs.js +0 -65
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { maskFencedAndQuotedText } from '../spec-format/unified-spec-builder.js';
|
|
2
|
+
import { RETIRED_SCAFFOLD_PREFIX } from '../spec-format/retired-scaffold.js';
|
|
1
3
|
/** Bounds the search for criteria to the frontmatter block only (between the two `---`
|
|
2
4
|
* delimiters), so criterion-shaped lines never leak in from the rendered body below it. */
|
|
3
5
|
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
|
|
@@ -21,7 +23,8 @@ const CRITERION_TEXT_RE = /^\s+- text:\s*"?(.+?)"?\s*$/gm;
|
|
|
21
23
|
* the criterion-equivalent for `scenarios:`-format specs, which have no `- text:` entries. */
|
|
22
24
|
const SCENARIO_TITLE_RE = /^\s+- title:\s*"?(.+?)"?\s*$/gm;
|
|
23
25
|
const FILE_LINE_RE = /^-\s+(.+?)\s+\((pending|done)\)\s*$/gm;
|
|
24
|
-
const
|
|
26
|
+
const SECTION_HEADING_RE = /^#{1,3}[ \t]+(.+?)[ \t]*$/gm;
|
|
27
|
+
const H3_HEADING_PREFIX_RE = /^###[ \t]/;
|
|
25
28
|
const BULLET_LINE_RE = /^[-*][ \t]+(.+)$/;
|
|
26
29
|
/** A bullet that, once a leading `Label:`, wrapping backticks, and a trailing `(status)`
|
|
27
30
|
* marker are stripped, is just a file path: contains a `/` and ends in a short extension.
|
|
@@ -98,9 +101,40 @@ export function checkGenericSpecOutput(content) {
|
|
|
98
101
|
...checkCriteria(criteria),
|
|
99
102
|
...checkTechnicalReferences(content),
|
|
100
103
|
...checkInterpolatedSections(content, criteria),
|
|
104
|
+
...checkRetiredFillerVocabulary(content),
|
|
101
105
|
];
|
|
102
106
|
return { passed: issues.length === 0, issues };
|
|
103
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* SPEC-1435: dedicated scan for the constant filler sentences the renderer and the
|
|
110
|
+
* unified spec builder used to emit whenever nothing was grounded. Those sentences
|
|
111
|
+
* satisfied structural checks (a non-empty subsection, an executable-criteria match)
|
|
112
|
+
* without carrying any spec-specific content, and no existing rule matched their exact
|
|
113
|
+
* wording. Scans the whole body — prose and bullets, any heading level — with fenced
|
|
114
|
+
* code blocks and quoted text masked the same way `unified-spec-builder.ts` masks them,
|
|
115
|
+
* so a legitimate example inside a fence or blockquote is never flagged.
|
|
116
|
+
*/
|
|
117
|
+
const RETIRED_FILLER_SENTENCES = [
|
|
118
|
+
'Implement only the grounded ownership and observable behavior declared by this contract.',
|
|
119
|
+
'No repository-supported verification command was grounded for this candidate.',
|
|
120
|
+
'No file ownership was grounded for this behavior.',
|
|
121
|
+
'No repository-supported verification evidence was grounded for these criteria.',
|
|
122
|
+
RETIRED_SCAFFOLD_PREFIX,
|
|
123
|
+
];
|
|
124
|
+
function checkRetiredFillerVocabulary(content) {
|
|
125
|
+
const scannable = maskFencedAndQuotedText(content);
|
|
126
|
+
const issues = [];
|
|
127
|
+
for (const sentence of RETIRED_FILLER_SENTENCES) {
|
|
128
|
+
if (scannable.includes(sentence)) {
|
|
129
|
+
issues.push({
|
|
130
|
+
kind: 'generic-technical-reference',
|
|
131
|
+
phrase: sentence,
|
|
132
|
+
reason: `retired filler text must not appear in a persisted spec: "${sentence}"`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return issues;
|
|
137
|
+
}
|
|
104
138
|
/**
|
|
105
139
|
* SPEC-1406 (DEFECT 2, round 2 — detector rebuilt around CRITERION RESTATEMENT): the
|
|
106
140
|
* previous prefix/suffix-length rule flagged `### File-Level Work Plan` (whose bullets
|
|
@@ -115,10 +149,10 @@ export function checkGenericSpecOutput(content) {
|
|
|
115
149
|
*/
|
|
116
150
|
function checkInterpolatedSections(content, criteria) {
|
|
117
151
|
const issues = [];
|
|
118
|
-
const headingMatches = [...content.matchAll(
|
|
152
|
+
const headingMatches = [...content.matchAll(SECTION_HEADING_RE)];
|
|
119
153
|
for (let i = 0; i < headingMatches.length; i += 1) {
|
|
120
154
|
const heading = headingMatches[i];
|
|
121
|
-
if (!heading?.[1]) {
|
|
155
|
+
if (!heading?.[1] || !H3_HEADING_PREFIX_RE.test(heading[0])) {
|
|
122
156
|
continue;
|
|
123
157
|
}
|
|
124
158
|
const start = heading.index + heading[0].length;
|
|
@@ -65,7 +65,10 @@ async function runEnricherFromSpecSection(opts) {
|
|
|
65
65
|
if (result.enriched && result.enrichedContent) {
|
|
66
66
|
const updated = `${specRaw.slice(0, section.contentStart)}${result.enrichedContent}${specRaw.slice(section.end)}`;
|
|
67
67
|
assertEnrichmentPreservesContract(specRaw, updated, 'review-enricher');
|
|
68
|
-
|
|
68
|
+
const relativeProposalPath = join('planu', '.runtime', 'enrichment-proposals', `${opts.specId}.md`);
|
|
69
|
+
const proposalPath = join(opts.projectPath, relativeProposalPath);
|
|
70
|
+
await atomicWriteFile(proposalPath, updated);
|
|
71
|
+
console.error(`Review enrichment proposal written to ${relativeProposalPath}`);
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// engine/type-safety-gate.ts — SPEC-968: Deterministic TypeScript anti-pattern scanner
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
2
|
import { readFileSync } from 'node:fs';
|
|
4
3
|
import { join } from 'node:path';
|
|
4
|
+
import { execGitCapture } from './git/exec-git.js';
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// Helpers
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
@@ -9,11 +9,7 @@ function getChangedTsFiles(projectPath) {
|
|
|
9
9
|
let names;
|
|
10
10
|
// Try origin/main...HEAD (typical feature branch)
|
|
11
11
|
try {
|
|
12
|
-
names =
|
|
13
|
-
cwd: projectPath,
|
|
14
|
-
encoding: 'utf-8',
|
|
15
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
16
|
-
})
|
|
12
|
+
names = execGitCapture('git diff --name-only --diff-filter=ACM origin/main...HEAD', projectPath)
|
|
17
13
|
.trim()
|
|
18
14
|
.split('\n')
|
|
19
15
|
.filter(Boolean);
|
|
@@ -21,11 +17,7 @@ function getChangedTsFiles(projectPath) {
|
|
|
21
17
|
catch {
|
|
22
18
|
// Fallback: last commit
|
|
23
19
|
try {
|
|
24
|
-
names =
|
|
25
|
-
cwd: projectPath,
|
|
26
|
-
encoding: 'utf-8',
|
|
27
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
28
|
-
})
|
|
20
|
+
names = execGitCapture('git diff --name-only --diff-filter=ACM HEAD~1..HEAD', projectPath)
|
|
29
21
|
.trim()
|
|
30
22
|
.split('\n')
|
|
31
23
|
.filter(Boolean);
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { planuDogfoodBugsRule } from './rules/planu-dogfood-bugs.js';
|
|
4
4
|
import { planuWorkflowRule } from './rules/planu-workflow.js';
|
|
5
5
|
import { planuModesRule } from './rules/planu-modes.js';
|
|
6
|
-
import { agentTeamsRule } from './rules/agent-teams.js';
|
|
7
6
|
import { planuEnglishSpecsRule } from './rules/planu-english-specs.js';
|
|
8
7
|
import { planuBddCriteriaRule } from './rules/planu-bdd-criteria.js';
|
|
9
8
|
import { planuApprovalGatesRule } from './rules/planu-approval-gates.js';
|
|
@@ -26,7 +25,6 @@ export const UNIVERSAL_RULES = [
|
|
|
26
25
|
planuMinimalChangeRule,
|
|
27
26
|
planuReleasePolicyRule,
|
|
28
27
|
planuModesRule,
|
|
29
|
-
agentTeamsRule,
|
|
30
28
|
planuDogfoodBugsRule,
|
|
31
29
|
planuDebateReviewRule,
|
|
32
30
|
planuRevertProofTestsRule,
|
|
@@ -48,7 +48,7 @@ Any new feature, bug fix, refactor, integration, or task > 5 min.
|
|
|
48
48
|
|------|------|
|
|
49
49
|
| New feature | \`facilitate("what to build")\` |
|
|
50
50
|
| Project status | \`planu_status\` |
|
|
51
|
-
| Find related specs | \`
|
|
51
|
+
| Find related specs | \`list_specs\` and scan titles |
|
|
52
52
|
| List approved work | \`list_specs\` with \`status: approved\` |
|
|
53
53
|
| Mark done | \`update_status\` → \`done\` |
|
|
54
54
|
| Verify | \`validate\` |
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { ValidationArtifactDigests, ValidationReceiptBindings } from '../../types/validation-receipt.js';
|
|
1
|
+
import type { ValidationArtifactDigests, ValidationArtifactProvenanceWriteResult, ValidationReceiptBindings } from '../../types/validation-receipt.js';
|
|
2
2
|
import type { DurableValidationBindings, ValidationPhase, ValidationPhaseGraphResult, ValidationPhaseProgress } from '../../types/durable-validation.js';
|
|
3
3
|
export type { DurableValidationBindings } from '../../types/durable-validation.js';
|
|
4
4
|
export { gitSourceTreeDigest } from './validation-source-digest.js';
|
|
5
5
|
export { createPendingValidationSubmission, createValidationSubmission, validationOperationId, validationSubmissionOperationId, } from './validation-submission.js';
|
|
6
|
+
export declare function absentArtifactDigest(kind: string): string;
|
|
6
7
|
export declare function toValidationReceiptBindings(bindings: DurableValidationBindings, artifactDigests: ValidationArtifactDigests): ValidationReceiptBindings;
|
|
7
8
|
/**
|
|
8
9
|
* Evidence artifacts (discovery/task-plan/traceability/contract-validation) may legitimately
|
|
@@ -28,7 +29,7 @@ export declare function writeValidationArtifactProvenance(input: {
|
|
|
28
29
|
readonly receiptId: string;
|
|
29
30
|
readonly issuerId: string;
|
|
30
31
|
readonly artifactDigests: ValidationArtifactDigests;
|
|
31
|
-
}): Promise<
|
|
32
|
+
}): Promise<ValidationArtifactProvenanceWriteResult>;
|
|
32
33
|
/** Redact progress before it crosses the durable checkpoint boundary. */
|
|
33
34
|
export declare function redactValidationProgress(progress: ValidationPhaseProgress): ValidationPhaseProgress;
|
|
34
35
|
/** Execute independent validation gates with one shared deadline and cancellation tree. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { technologyValue } from '../technology-registry.js';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
5
|
import { runAbortableProcess } from '../abortable-process-runner.js';
|
|
6
6
|
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
@@ -15,6 +15,13 @@ const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
|
|
15
15
|
const DEFAULT_MAX_CONCURRENCY = 2;
|
|
16
16
|
const MAX_CONCURRENCY = 8;
|
|
17
17
|
const MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
18
|
+
const APPROVED_SPEC_MISSING_LABEL = 'approved-spec';
|
|
19
|
+
export function absentArtifactDigest(kind) {
|
|
20
|
+
return evidenceArtifactDigest({ missing: kind });
|
|
21
|
+
}
|
|
22
|
+
function missingFileDigest(label) {
|
|
23
|
+
return digest(`missing:${label}`);
|
|
24
|
+
}
|
|
18
25
|
function assertBindings(bindings) {
|
|
19
26
|
if (!CANONICAL_PROJECT_ID.test(bindings.projectId)) {
|
|
20
27
|
throw new Error('[Planu] Durable validation requires a canonical projectId');
|
|
@@ -47,6 +54,18 @@ export function toValidationReceiptBindings(bindings, artifactDigests) {
|
|
|
47
54
|
artifactDigests,
|
|
48
55
|
};
|
|
49
56
|
}
|
|
57
|
+
function codeBindingsMatch(observed, stored) {
|
|
58
|
+
return (observed.projectId === stored.projectId &&
|
|
59
|
+
observed.specId === stored.specId &&
|
|
60
|
+
observed.sourceDigest === stored.sourceDigest &&
|
|
61
|
+
observed.specDigest === stored.specDigest &&
|
|
62
|
+
observed.policyDigest === stored.policyDigest &&
|
|
63
|
+
observed.toolchainDigest === stored.toolchainDigest &&
|
|
64
|
+
observed.artifactDigests.approvedSpec === stored.artifactDigests.approvedSpec);
|
|
65
|
+
}
|
|
66
|
+
function digestRebindsToSentinel(observedDigest, storedDigest, kind) {
|
|
67
|
+
return observedDigest !== storedDigest && observedDigest === absentArtifactDigest(kind);
|
|
68
|
+
}
|
|
50
69
|
/**
|
|
51
70
|
* Evidence artifacts (discovery/task-plan/traceability/contract-validation) may legitimately
|
|
52
71
|
* be regenerated after a receipt was issued (e.g. autopilot skeleton fill). If ONLY the evidence
|
|
@@ -57,20 +76,22 @@ export function doneReceiptBindingsDifferOnlyInEvidence(observed, stored) {
|
|
|
57
76
|
if (!observed?.artifactDigests || !stored?.artifactDigests) {
|
|
58
77
|
return false;
|
|
59
78
|
}
|
|
60
|
-
|
|
61
|
-
observed.specId === stored.specId &&
|
|
62
|
-
observed.sourceDigest === stored.sourceDigest &&
|
|
63
|
-
observed.specDigest === stored.specDigest &&
|
|
64
|
-
observed.policyDigest === stored.policyDigest &&
|
|
65
|
-
observed.toolchainDigest === stored.toolchainDigest &&
|
|
66
|
-
observed.artifactDigests.approvedSpec === stored.artifactDigests.approvedSpec;
|
|
67
|
-
if (!codeBindingsMatch) {
|
|
79
|
+
if (!codeBindingsMatch(observed, stored)) {
|
|
68
80
|
return false;
|
|
69
81
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
const observedArtifacts = observed.artifactDigests;
|
|
83
|
+
const storedArtifacts = stored.artifactDigests;
|
|
84
|
+
const evidenceDiffers = observedArtifacts.discovery !== storedArtifacts.discovery ||
|
|
85
|
+
observedArtifacts.taskPlan !== storedArtifacts.taskPlan ||
|
|
86
|
+
observedArtifacts.traceability !== storedArtifacts.traceability ||
|
|
87
|
+
observedArtifacts.contractValidation !== storedArtifacts.contractValidation;
|
|
88
|
+
if (!evidenceDiffers) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const rebindsToAbsentEvidence = digestRebindsToSentinel(observedArtifacts.discovery, storedArtifacts.discovery, 'discovery') ||
|
|
92
|
+
digestRebindsToSentinel(observedArtifacts.taskPlan, storedArtifacts.taskPlan, 'task-plan') ||
|
|
93
|
+
digestRebindsToSentinel(observedArtifacts.traceability, storedArtifacts.traceability, 'traceability');
|
|
94
|
+
return !rebindsToAbsentEvidence;
|
|
74
95
|
}
|
|
75
96
|
async function digestFile(path, missingLabel) {
|
|
76
97
|
try {
|
|
@@ -78,7 +99,7 @@ async function digestFile(path, missingLabel) {
|
|
|
78
99
|
}
|
|
79
100
|
catch (error) {
|
|
80
101
|
if (error.code === 'ENOENT') {
|
|
81
|
-
return
|
|
102
|
+
return missingFileDigest(missingLabel);
|
|
82
103
|
}
|
|
83
104
|
throw error;
|
|
84
105
|
}
|
|
@@ -101,7 +122,7 @@ async function readEvidenceFile(path) {
|
|
|
101
122
|
/** Compute immutable validation and lifecycle-artifact bindings without trusting spec metadata. */
|
|
102
123
|
export async function computeDurableValidationBindings(input) {
|
|
103
124
|
const handoff = join(projectDataDir(input.canonicalProjectId), 'handoffs', input.specId);
|
|
104
|
-
const specHash = await digestFile(input.specPath,
|
|
125
|
+
const specHash = await digestFile(input.specPath, APPROVED_SPEC_MISSING_LABEL);
|
|
105
126
|
const [discovery, taskPlan, traceability, ...contracts] = await Promise.all([
|
|
106
127
|
readEvidenceFile(join(handoff, 'discovery.json')),
|
|
107
128
|
readEvidenceFile(join(handoff, 'task-plan.json')),
|
|
@@ -136,19 +157,76 @@ export async function computeDurableValidationBindings(input) {
|
|
|
136
157
|
},
|
|
137
158
|
artifactDigests: {
|
|
138
159
|
approvedSpec: specHash,
|
|
139
|
-
discovery: discovery
|
|
140
|
-
|
|
141
|
-
: evidenceArtifactDigest({ missing: 'discovery' }),
|
|
142
|
-
taskPlan: taskPlan
|
|
143
|
-
? evidenceArtifactDigest(taskPlan)
|
|
144
|
-
: evidenceArtifactDigest({ missing: 'task-plan' }),
|
|
160
|
+
discovery: discovery ? evidenceArtifactDigest(discovery) : absentArtifactDigest('discovery'),
|
|
161
|
+
taskPlan: taskPlan ? evidenceArtifactDigest(taskPlan) : absentArtifactDigest('task-plan'),
|
|
145
162
|
traceability: traceability
|
|
146
163
|
? evidenceArtifactDigest(traceability)
|
|
147
|
-
:
|
|
164
|
+
: absentArtifactDigest('traceability'),
|
|
148
165
|
contractValidation: evidenceArtifactCollectionDigest(contractArtifacts),
|
|
149
166
|
},
|
|
150
167
|
};
|
|
151
168
|
}
|
|
169
|
+
async function payloadFileExists(path) {
|
|
170
|
+
try {
|
|
171
|
+
await stat(path);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (error.code === 'ENOENT') {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function perFileArtifacts(artifactDigests) {
|
|
182
|
+
return [
|
|
183
|
+
{
|
|
184
|
+
name: 'approved-spec',
|
|
185
|
+
digestValue: artifactDigests.approvedSpec,
|
|
186
|
+
sentinel: missingFileDigest(APPROVED_SPEC_MISSING_LABEL),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'discovery',
|
|
190
|
+
digestValue: artifactDigests.discovery,
|
|
191
|
+
sentinel: absentArtifactDigest('discovery'),
|
|
192
|
+
payloadFile: 'discovery.json',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'task-plan',
|
|
196
|
+
digestValue: artifactDigests.taskPlan,
|
|
197
|
+
sentinel: absentArtifactDigest('task-plan'),
|
|
198
|
+
payloadFile: 'task-plan.json',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'traceability-matrix',
|
|
202
|
+
digestValue: artifactDigests.traceability,
|
|
203
|
+
sentinel: absentArtifactDigest('traceability'),
|
|
204
|
+
payloadFile: 'traceability-matrix.json',
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
async function writeProvenanceSidecars(directory, entries, identity) {
|
|
209
|
+
const absentArtifacts = [];
|
|
210
|
+
const entriesToWrite = [];
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (entry.digestValue === entry.sentinel) {
|
|
213
|
+
absentArtifacts.push(entry.name);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (entry.payloadFile !== undefined &&
|
|
217
|
+
!(await payloadFileExists(join(directory, entry.payloadFile)))) {
|
|
218
|
+
throw new Error(`[Planu] Validation artifact provenance digest for ${entry.name} references a payload that does not exist`);
|
|
219
|
+
}
|
|
220
|
+
entriesToWrite.push(entry);
|
|
221
|
+
}
|
|
222
|
+
const writtenPaths = [];
|
|
223
|
+
for (const entry of entriesToWrite) {
|
|
224
|
+
const path = join(directory, `${entry.name}.provenance.json`);
|
|
225
|
+
await atomicWriteFile(path, `${JSON.stringify({ ...identity, artifactDigest: entry.digestValue }, null, 2)}\n`);
|
|
226
|
+
writtenPaths.push(path);
|
|
227
|
+
}
|
|
228
|
+
return { writtenPaths, absentArtifacts };
|
|
229
|
+
}
|
|
152
230
|
/** Persist content-addressed receipt links beside machine-local lifecycle artifacts. */
|
|
153
231
|
export async function writeValidationArtifactProvenance(input) {
|
|
154
232
|
assertBindings({
|
|
@@ -163,24 +241,12 @@ export async function writeValidationArtifactProvenance(input) {
|
|
|
163
241
|
throw new Error('[Planu] Invalid validation artifact provenance identity');
|
|
164
242
|
}
|
|
165
243
|
const directory = join(projectDataDir(input.canonicalProjectId), 'handoffs', input.specId);
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
];
|
|
173
|
-
const sidecars = await Promise.all(entries.map(async ([name, artifactDigest]) => {
|
|
174
|
-
const path = join(directory, `${name}.provenance.json`);
|
|
175
|
-
await atomicWriteFile(path, `${JSON.stringify({
|
|
176
|
-
receiptId: input.receiptId,
|
|
177
|
-
artifactDigest,
|
|
178
|
-
specId: input.specId,
|
|
179
|
-
issuerId: input.issuerId,
|
|
180
|
-
}, null, 2)}\n`);
|
|
181
|
-
return path;
|
|
182
|
-
}));
|
|
183
|
-
return sidecars;
|
|
244
|
+
const identity = { receiptId: input.receiptId, specId: input.specId, issuerId: input.issuerId };
|
|
245
|
+
const { writtenPaths, absentArtifacts } = await writeProvenanceSidecars(directory, perFileArtifacts(input.artifactDigests), identity);
|
|
246
|
+
const contractSidecarPath = join(directory, 'contract-validation.provenance.json');
|
|
247
|
+
await atomicWriteFile(contractSidecarPath, `${JSON.stringify({ ...identity, artifactDigest: input.artifactDigests.contractValidation }, null, 2)}\n`);
|
|
248
|
+
writtenPaths.push(contractSidecarPath);
|
|
249
|
+
return { writtenPaths, absentArtifacts };
|
|
184
250
|
}
|
|
185
251
|
/** Redact progress before it crosses the durable checkpoint boundary. */
|
|
186
252
|
export function redactValidationProgress(progress) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ValidationFreshnessLease, ValidationReceiptBindings, ValidationWatchEventClassification } from '../../types/validation-receipt.js';
|
|
1
|
+
import type { ValidationFreshnessBarrierResult, ValidationFreshnessLease, ValidationFreshnessRebaselineResult, ValidationReceiptBindings, ValidationWatchEventClassification } from '../../types/validation-receipt.js';
|
|
2
2
|
export type { ValidationFreshnessLease } from '../../types/validation-receipt.js';
|
|
3
3
|
/**
|
|
4
4
|
* Invalidate the active validation lease for a spec.
|
|
@@ -49,6 +49,7 @@ export declare function closeValidationFreshnessLease(lease: Pick<ValidationFres
|
|
|
49
49
|
/** Bind the post-watch snapshot; an event before this call makes the lease unusable. */
|
|
50
50
|
export declare function bindValidationFreshnessLease(lease: ValidationFreshnessLease, bindings: ValidationReceiptBindings): Promise<boolean>;
|
|
51
51
|
/** Drain invalidation work and verify epoch, generation, health, expiry and bound inputs. */
|
|
52
|
+
export declare function evaluateValidationFreshnessBarrier(lease: ValidationFreshnessLease, bindings?: ValidationReceiptBindings): Promise<ValidationFreshnessBarrierResult>;
|
|
52
53
|
export declare function assertValidationFreshnessBarrier(lease: ValidationFreshnessLease, bindings?: ValidationReceiptBindings): Promise<boolean>;
|
|
53
54
|
export declare function captureValidationFreshnessLease(identity: {
|
|
54
55
|
projectId: string;
|
|
@@ -59,7 +60,7 @@ export declare function rebaselineValidationFreshnessLeaseAfterRecompute(input:
|
|
|
59
60
|
lease: ValidationFreshnessLease;
|
|
60
61
|
expectedBindings: ValidationReceiptBindings;
|
|
61
62
|
recompute: () => Promise<ValidationReceiptBindings>;
|
|
62
|
-
}): Promise<
|
|
63
|
+
}): Promise<ValidationFreshnessRebaselineResult>;
|
|
63
64
|
export declare function resetValidationFreshnessForTests(): Promise<void>;
|
|
64
65
|
/** Count-only process diagnostic; intentionally absent from the package entrypoint. */
|
|
65
66
|
export declare function getValidationFreshnessDiagnosticsForTests(): {
|
|
@@ -635,15 +635,15 @@ async function readStableState(lease, expectedGeneration) {
|
|
|
635
635
|
return undefined;
|
|
636
636
|
}
|
|
637
637
|
/** Drain invalidation work and verify epoch, generation, health, expiry and bound inputs. */
|
|
638
|
-
export async function
|
|
638
|
+
export async function evaluateValidationFreshnessBarrier(lease, bindings) {
|
|
639
639
|
const candidate = active.get(key(lease.projectId, lease.specId));
|
|
640
640
|
if (!candidate?.healthy ||
|
|
641
641
|
candidate.epoch !== lease.epoch ||
|
|
642
642
|
candidate.generation !== lease.generation) {
|
|
643
|
-
return false;
|
|
643
|
+
return { fresh: false, reason: 'lease-superseded' };
|
|
644
644
|
}
|
|
645
645
|
if (!(await drainAmbiguousEvents(lease, candidate, candidate.bindings))) {
|
|
646
|
-
return false;
|
|
646
|
+
return { fresh: false, reason: 'invalidation-drain-failed' };
|
|
647
647
|
}
|
|
648
648
|
let fingerprintBefore;
|
|
649
649
|
try {
|
|
@@ -652,23 +652,23 @@ export async function assertValidationFreshnessBarrier(lease, bindings) {
|
|
|
652
652
|
catch (error) {
|
|
653
653
|
reportClassifiedDegradation('VALIDATION_FRESHNESS_CHECK_FAILED', error);
|
|
654
654
|
invalidateExactEpoch(lease, candidate);
|
|
655
|
-
return false;
|
|
655
|
+
return { fresh: false, reason: 'evidence-check-failed', changedInput: candidate.handoffPath };
|
|
656
656
|
}
|
|
657
657
|
if (fingerprintBefore !== candidate.evidenceFingerprint) {
|
|
658
658
|
invalidateExactEpoch(lease, candidate);
|
|
659
|
-
return false;
|
|
659
|
+
return { fresh: false, reason: 'evidence-changed', changedInput: candidate.handoffPath };
|
|
660
660
|
}
|
|
661
661
|
try {
|
|
662
662
|
if (candidate.bindings &&
|
|
663
663
|
(await gitSourceTreeDigest(candidate.projectPath)) !== candidate.bindings.sourceDigest) {
|
|
664
664
|
invalidateExactEpoch(lease, candidate);
|
|
665
|
-
return false;
|
|
665
|
+
return { fresh: false, reason: 'source-changed', changedInput: candidate.projectPath };
|
|
666
666
|
}
|
|
667
667
|
}
|
|
668
668
|
catch (error) {
|
|
669
669
|
reportClassifiedDegradation('VALIDATION_FRESHNESS_CHECK_FAILED', error);
|
|
670
670
|
invalidateExactEpoch(lease, candidate);
|
|
671
|
-
return false;
|
|
671
|
+
return { fresh: false, reason: 'source-check-failed', changedInput: candidate.projectPath };
|
|
672
672
|
}
|
|
673
673
|
// A generation change invalidates the lease synchronously. Do not wait for
|
|
674
674
|
// the durable receipt cleanup when the in-memory barrier already fails
|
|
@@ -684,15 +684,21 @@ export async function assertValidationFreshnessBarrier(lease, bindings) {
|
|
|
684
684
|
}
|
|
685
685
|
if (!state || fingerprintAfter !== fingerprintBefore) {
|
|
686
686
|
invalidateExactEpoch(lease, candidate);
|
|
687
|
-
return false;
|
|
687
|
+
return { fresh: false, reason: 'state-lost', changedInput: candidate.handoffPath };
|
|
688
688
|
}
|
|
689
689
|
if (state.generation !== lease.generation) {
|
|
690
|
-
return false;
|
|
690
|
+
return { fresh: false, reason: 'lease-superseded' };
|
|
691
691
|
}
|
|
692
692
|
if (Date.now() >= state.expiresAt) {
|
|
693
|
-
return false;
|
|
693
|
+
return { fresh: false, reason: 'lease-expired' };
|
|
694
|
+
}
|
|
695
|
+
if (bindings !== undefined && JSON.stringify(state.bindings) !== JSON.stringify(bindings)) {
|
|
696
|
+
return { fresh: false, reason: 'bindings-mismatch' };
|
|
694
697
|
}
|
|
695
|
-
return
|
|
698
|
+
return { fresh: true };
|
|
699
|
+
}
|
|
700
|
+
export async function assertValidationFreshnessBarrier(lease, bindings) {
|
|
701
|
+
return (await evaluateValidationFreshnessBarrier(lease, bindings)).fresh;
|
|
696
702
|
}
|
|
697
703
|
export async function captureValidationFreshnessLease(identity, bindings) {
|
|
698
704
|
const state = active.get(key(identity.projectId, identity.specId));
|
|
@@ -707,7 +713,7 @@ export async function rebaselineValidationFreshnessLeaseAfterRecompute(input) {
|
|
|
707
713
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
708
714
|
const before = await readStableState(input.lease);
|
|
709
715
|
if (!before || Date.now() >= before.expiresAt) {
|
|
710
|
-
return
|
|
716
|
+
return { rejection: { reason: 'lease-expired' } };
|
|
711
717
|
}
|
|
712
718
|
const generationBefore = before.generation;
|
|
713
719
|
let fingerprintBefore;
|
|
@@ -721,14 +727,14 @@ export async function rebaselineValidationFreshnessLeaseAfterRecompute(input) {
|
|
|
721
727
|
catch (error) {
|
|
722
728
|
reportClassifiedDegradation('VALIDATION_FRESHNESS_CHECK_FAILED', error);
|
|
723
729
|
invalidateExactEpoch(input.lease, before);
|
|
724
|
-
return
|
|
730
|
+
return { rejection: { reason: 'recompute-failed', changedInput: before.handoffPath } };
|
|
725
731
|
}
|
|
726
732
|
const after = await readStableState(input.lease);
|
|
727
733
|
if (after?.generation !== generationBefore || fingerprintBefore !== fingerprintAfter) {
|
|
728
734
|
continue;
|
|
729
735
|
}
|
|
730
736
|
if (JSON.stringify(input.expectedBindings) !== JSON.stringify(observedBindings)) {
|
|
731
|
-
return
|
|
737
|
+
return { rejection: { reason: 'bindings-mismatch' } };
|
|
732
738
|
}
|
|
733
739
|
const confirmed = await readStableState(input.lease);
|
|
734
740
|
if (confirmed?.generation !== after.generation) {
|
|
@@ -736,9 +742,9 @@ export async function rebaselineValidationFreshnessLeaseAfterRecompute(input) {
|
|
|
736
742
|
}
|
|
737
743
|
confirmed.bindings = structuredClone(observedBindings);
|
|
738
744
|
confirmed.evidenceFingerprint = fingerprintAfter;
|
|
739
|
-
return { ...input.lease, generation: confirmed.generation };
|
|
745
|
+
return { lease: { ...input.lease, generation: confirmed.generation } };
|
|
740
746
|
}
|
|
741
|
-
return
|
|
747
|
+
return { rejection: { reason: 'lease-unstable' } };
|
|
742
748
|
}
|
|
743
749
|
export async function resetValidationFreshnessForTests() {
|
|
744
750
|
const leases = [...active.entries()].map(([identity, state]) => {
|
|
@@ -9,6 +9,7 @@ const runFile = promisify(execFile);
|
|
|
9
9
|
const ROOT_ID = /^[a-f0-9]{64}$/u;
|
|
10
10
|
const SAFE_SPEC_ID = /^SPEC-[1-9]\d*$/u;
|
|
11
11
|
const PORTABLE_SPEC = /^planu\/specs\/(SPEC-\d{3,}(?:-[^/]+)?)\/spec\.md$/u;
|
|
12
|
+
const STALE_SPEC_TAIL = /planu[/\\]specs[/\\]([^/\\]+)[/\\]spec\.md$/u;
|
|
12
13
|
export class ValidationWorktreeError extends Error {
|
|
13
14
|
code;
|
|
14
15
|
specId;
|
|
@@ -83,6 +84,35 @@ async function resolveGitRoot(projectPath, specId) {
|
|
|
83
84
|
}
|
|
84
85
|
return membership;
|
|
85
86
|
}
|
|
87
|
+
function derivePortableSuffix(specPath, specId) {
|
|
88
|
+
const match = STALE_SPEC_TAIL.exec(specPath);
|
|
89
|
+
const dir = match?.[1];
|
|
90
|
+
if (!dir || (dir !== specId && !dir.startsWith(`${specId}-`))) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const portableSuffix = `planu/specs/${dir}/spec.md`;
|
|
94
|
+
return PORTABLE_SPEC.test(portableSuffix) ? portableSuffix : undefined;
|
|
95
|
+
}
|
|
96
|
+
async function classifyStaleSpecPath(specPath, specId) {
|
|
97
|
+
if (!isAbsolute(specPath)) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const portableSuffix = derivePortableSuffix(specPath, specId);
|
|
101
|
+
if (!portableSuffix) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await lstat(dirname(specPath));
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (isEnoent(error)) {
|
|
110
|
+
return portableSuffix;
|
|
111
|
+
}
|
|
112
|
+
reportClassifiedDegradation('VALIDATION_STALE_PREFIX_PROBE_FAILED', error);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
86
116
|
async function resolveSpecSourceRoot(input) {
|
|
87
117
|
const specPath = isAbsolute(input.canonicalSpecPath)
|
|
88
118
|
? input.canonicalSpecPath
|
|
@@ -92,6 +122,10 @@ async function resolveSpecSourceRoot(input) {
|
|
|
92
122
|
source = await resolveGitMembership(dirname(specPath), input.specId);
|
|
93
123
|
}
|
|
94
124
|
catch {
|
|
125
|
+
const portableSuffix = await classifyStaleSpecPath(specPath, input.specId);
|
|
126
|
+
if (portableSuffix) {
|
|
127
|
+
throw new ValidationWorktreeError('VALIDATION_STALE_SPEC_PATH', input.specId, portableSuffix);
|
|
128
|
+
}
|
|
95
129
|
throw new ValidationWorktreeError('VALIDATION_SPEC_PATH_TRAVERSAL', input.specId);
|
|
96
130
|
}
|
|
97
131
|
if (source.gitCommonDir !== input.canonical.gitCommonDir) {
|
|
@@ -87,7 +87,7 @@ function buildExecuteSddFlowPrompt(specId, projectPath, mode) {
|
|
|
87
87
|
const branchStep = mode !== 'implement-only'
|
|
88
88
|
? '4. Create a feature branch: `git checkout -b feat/' + specId + '-<slug>`.'
|
|
89
89
|
: '4. Implement in the current branch (implement-only mode).';
|
|
90
|
-
const prStep = mode !== 'implement-only' ? '12. Open a PR with `
|
|
90
|
+
const prStep = mode !== 'implement-only' ? '12. Open a PR with `gh pr create`.' : '';
|
|
91
91
|
return [
|
|
92
92
|
`You are implementing Planu spec \`${specId}\`${pathHint}${modeHint} using the full SDD flow.`,
|
|
93
93
|
'',
|
|
@@ -104,7 +104,7 @@ function buildExecuteSddFlowPrompt(specId, projectPath, mode) {
|
|
|
104
104
|
'8. Call `validate({ specId: "' +
|
|
105
105
|
specId +
|
|
106
106
|
'" })` and capture the durable acknowledgement fields: `operationId`, `projectId`, `workspaceId`, and `cursor`.',
|
|
107
|
-
'9.
|
|
107
|
+
'9. Poll `get_job` with those identifiers and the latest cursor. Repeat while the job is non-terminal; do not treat an accepted or running job as a validation result.',
|
|
108
108
|
'10. Require terminal state `completed`, then inspect `job.result.structuredContent`. If the job is failed, dead-letter, or cancelled, stop and report it. Fix every failing criterion and submit a new validation job when needed.',
|
|
109
109
|
'11. Call `update_status({ specId: "' +
|
|
110
110
|
specId +
|
|
@@ -118,18 +118,18 @@ function buildExecuteSddFlowPrompt(specId, projectPath, mode) {
|
|
|
118
118
|
}
|
|
119
119
|
function buildChallengeSpecPrompt(specId, focus) {
|
|
120
120
|
const focusHint = focus !== undefined && focus !== 'all' ? ` with a focus on **${focus}**` : '';
|
|
121
|
-
const
|
|
122
|
-
? '
|
|
121
|
+
const securityHint = focus === 'security' || focus === 'all'
|
|
122
|
+
? ' Pay special attention to adversarial and security-relevant scenarios.'
|
|
123
123
|
: '';
|
|
124
124
|
return [
|
|
125
125
|
`You are stress-testing Planu spec \`${specId}\`${focusHint} to find weaknesses before implementation.`,
|
|
126
126
|
'',
|
|
127
127
|
'Steps:',
|
|
128
128
|
`1. Call \`list_specs\` and read the full spec for \`${specId}\`.`,
|
|
129
|
-
`2. Call \`challenge_spec({ specId: "${specId}" })\` and read all findings
|
|
130
|
-
'3. Call `
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
`2. Call \`challenge_spec({ specId: "${specId}" })\` and read all findings.${securityHint}`,
|
|
130
|
+
'3. Call `check_readiness({ specId: "' +
|
|
131
|
+
specId +
|
|
132
|
+
'" })` to verify the spec has enough detail to implement.',
|
|
133
133
|
'',
|
|
134
134
|
'For each finding, provide:',
|
|
135
135
|
'- Severity: 🔴 blocking | 🟡 missing edge case | 🟢 ambiguity',
|
package/dist/index.js
CHANGED
|
@@ -83,6 +83,9 @@ function scheduleStartupStateRecovery(startupSync, recoverPendingPostCommitTasks
|
|
|
83
83
|
void import('./engine/execution/validate-job-executor.js').then(({ recoverValidateJobsAtStartup }) => {
|
|
84
84
|
recoverValidateJobsAtStartup();
|
|
85
85
|
});
|
|
86
|
+
void import('./engine/execution/package-handoff-job-executor.js').then(({ recoverPackageHandoffJobsAtStartup }) => {
|
|
87
|
+
recoverPackageHandoffJobsAtStartup();
|
|
88
|
+
});
|
|
86
89
|
}
|
|
87
90
|
const MCP_HOST_MARKERS = [
|
|
88
91
|
['claude-code', 'claude-code'],
|
|
@@ -139,23 +142,6 @@ async function setupPostHandshake(handshakeGate, officialToolNames) {
|
|
|
139
142
|
checkForUpdates().catch(() => {
|
|
140
143
|
/* best-effort */
|
|
141
144
|
});
|
|
142
|
-
// SPEC-566: Auto-configure Claude Code keybindings (global, one-time)
|
|
143
|
-
void (async () => {
|
|
144
|
-
try {
|
|
145
|
-
const { globalStore: gs } = await import('./storage/index.js');
|
|
146
|
-
const cfg = await gs.getGlobalConfig();
|
|
147
|
-
if (!cfg.keybindingsConfigured) {
|
|
148
|
-
const { configureClaudeKeybindings } = await import('./engine/claude-setup/keybindings-configurator.js');
|
|
149
|
-
const configured = await configureClaudeKeybindings();
|
|
150
|
-
if (configured) {
|
|
151
|
-
await gs.updateGlobalConfig({ keybindingsConfigured: true });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
/* best-effort */
|
|
157
|
-
}
|
|
158
|
-
})();
|
|
159
145
|
// SPEC-334: Sync planu/ YAML frontmatter status → data/ store (non-blocking)
|
|
160
146
|
scheduleStartupStateRecovery(startupSync, recoverPendingPostCommitTasksAtStartup);
|
|
161
147
|
// SPEC-1271: bounded, fair, durable spec.created outbox recovery — fire-and-forget so a
|