@planu/cli 5.7.5 → 5.7.6
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 +75 -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-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 +1 -1
- 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/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 +27 -0
- 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 +30 -10
- 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.js +115 -1
- 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/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/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/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-sdd-tools.js +0 -23
- package/dist/tools/register-spec-tools/core-spec-tools.js +18 -27
- 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 +1 -140
- package/dist/tools/tool-registry/group-infra.js +2 -39
- package/dist/tools/tool-registry/group-quality-compliance.js +9 -1
- 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 +12 -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
|
@@ -16,6 +16,10 @@ async function findImplementingActor(specId, projectId) {
|
|
|
16
16
|
}
|
|
17
17
|
return undefined;
|
|
18
18
|
}
|
|
19
|
+
const PLACEHOLDER_REVIEWER_SESSION_IDS = new Set(['system', 'planu-implementation-reviewer']);
|
|
20
|
+
function isVerifiableReviewerSessionId(sessionId) {
|
|
21
|
+
return Boolean(sessionId) && !PLACEHOLDER_REVIEWER_SESSION_IDS.has(sessionId ?? '');
|
|
22
|
+
}
|
|
19
23
|
export async function readVerifiedImplementationReview(projectId, specId) {
|
|
20
24
|
try {
|
|
21
25
|
const result = await readArtifact({ projectId, specId, kind: 'implementation_review' });
|
|
@@ -55,10 +59,19 @@ export async function readVerifiedImplementationReview(projectId, specId) {
|
|
|
55
59
|
readError: err instanceof Error ? err.message : String(err),
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
|
-
|
|
62
|
+
const reviewerSessionId = review.reviewer.sessionId?.trim();
|
|
63
|
+
if (!isVerifiableReviewerSessionId(reviewerSessionId) || implementerActor === undefined) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
reason: 'identity_unverifiable',
|
|
67
|
+
review,
|
|
68
|
+
implementingActor: implementerActor,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (reviewerSessionId === implementerActor) {
|
|
59
72
|
return { ok: false, reason: 'self_review', review, implementingActor: implementerActor };
|
|
60
73
|
}
|
|
61
|
-
return { ok: true, review, implementingActor: implementerActor
|
|
74
|
+
return { ok: true, review, implementingActor: implementerActor };
|
|
62
75
|
}
|
|
63
76
|
catch (err) {
|
|
64
77
|
/* reliability-optional: IMPLEMENTATION_REVIEW_GATE_READ — typed gate error blocks done */
|
|
@@ -70,6 +70,7 @@ export declare const ImplementationReviewV1Schema: z.ZodObject<{
|
|
|
70
70
|
approved: "approved";
|
|
71
71
|
"changes-requested": "changes-requested";
|
|
72
72
|
}>;
|
|
73
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
73
74
|
}, z.core.$strip>;
|
|
74
75
|
blockers: z.ZodArray<z.ZodString>;
|
|
75
76
|
suggestions: z.ZodArray<z.ZodString>;
|
|
@@ -356,6 +357,7 @@ export declare const ARTIFACT_SCHEMAS: {
|
|
|
356
357
|
approved: "approved";
|
|
357
358
|
"changes-requested": "changes-requested";
|
|
358
359
|
}>;
|
|
360
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
359
361
|
}, z.core.$strip>;
|
|
360
362
|
blockers: z.ZodArray<z.ZodString>;
|
|
361
363
|
suggestions: z.ZodArray<z.ZodString>;
|
|
@@ -47,6 +47,7 @@ export const ImplementationReviewV1Schema = z.object({
|
|
|
47
47
|
kind: z.enum(['implementation-review-agent', 'human']),
|
|
48
48
|
agent: z.string().min(1),
|
|
49
49
|
verdict: z.enum(['approved', 'changes-requested']),
|
|
50
|
+
sessionId: z.string().optional(),
|
|
50
51
|
}),
|
|
51
52
|
blockers: z.array(z.string()),
|
|
52
53
|
suggestions: z.array(z.string()),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { HandoffPackage, TokenWasteReport, MinimalImplementationReport } from '../types/index.js';
|
|
2
|
+
export declare function formatHandoff(pkg: HandoffPackage): string;
|
|
3
|
+
export declare function topItems<T>(items: T[], limit: number): T[];
|
|
4
|
+
export declare function buildHandoffSummaryText(args: {
|
|
5
|
+
pkg: HandoffPackage;
|
|
6
|
+
topBlockers: string[];
|
|
7
|
+
topRisks: string[];
|
|
8
|
+
minimalityBlocked: boolean | null;
|
|
9
|
+
tokenWasteRiskCount: number | null;
|
|
10
|
+
}): string;
|
|
11
|
+
export declare function summarizeMinimality(report: MinimalImplementationReport | null): Record<string, unknown> | undefined;
|
|
12
|
+
export declare function summarizeTokenWaste(report: TokenWasteReport | null): Record<string, unknown> | undefined;
|
|
13
|
+
export declare function summarizeGraphContext(graphContext: unknown): Record<string, unknown>;
|
|
14
|
+
export declare function buildHandoffTokenWasteReport(pkg: HandoffPackage, knowledge: {
|
|
15
|
+
projectPath?: string;
|
|
16
|
+
}, spec: {
|
|
17
|
+
id?: string;
|
|
18
|
+
risk?: string;
|
|
19
|
+
scope?: string;
|
|
20
|
+
target?: string;
|
|
21
|
+
}): Promise<TokenWasteReport | null>;
|
|
22
|
+
export declare function buildGraphContextSection(args: {
|
|
23
|
+
projectId: string;
|
|
24
|
+
projectPath?: string;
|
|
25
|
+
specId: string;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
text: string;
|
|
28
|
+
structured: unknown;
|
|
29
|
+
} | null>;
|
|
30
|
+
export declare function buildMinimalImplementationReport(pkg: HandoffPackage, knowledge: {
|
|
31
|
+
projectPath?: string;
|
|
32
|
+
}, spec: {
|
|
33
|
+
id?: string;
|
|
34
|
+
risk?: string;
|
|
35
|
+
}, handoffText: string): Promise<MinimalImplementationReport | null>;
|
|
36
|
+
//# sourceMappingURL=handoff-format.d.ts.map
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// engine/handoff-format.ts — Handoff package formatting and enrichment summaries (SPEC-1240)
|
|
2
|
+
// Relocated from tools/package-handoff.ts so the durable executor (engine layer) can render
|
|
3
|
+
// results without importing across the tools/engine boundary.
|
|
4
|
+
import { analyzeContextPreflight, buildTokenWasteReport, loadTokenWastePolicy, recommendRelevantTools, toolsFromPolicyGroups, } from './token-optimizer/index.js';
|
|
5
|
+
import { formatProjectGraphContext, queryProjectGraphSlice } from './project-graph/index.js';
|
|
6
|
+
import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from './minimality/index.js';
|
|
7
|
+
export function formatHandoff(pkg) {
|
|
8
|
+
const lines = [];
|
|
9
|
+
lines.push(`# Implementation Handoff Package — ${pkg.specId}`);
|
|
10
|
+
lines.push('');
|
|
11
|
+
lines.push(`Generated: ${pkg.generatedAt}`);
|
|
12
|
+
lines.push(`Readiness Score: ${pkg.readinessScore}/100`);
|
|
13
|
+
if (pkg.handoffPath) {
|
|
14
|
+
lines.push(`Handoff Path: ${pkg.handoffPath}`);
|
|
15
|
+
}
|
|
16
|
+
if (pkg.contextHash) {
|
|
17
|
+
lines.push(`Context Hash: ${pkg.contextHash}`);
|
|
18
|
+
}
|
|
19
|
+
lines.push(`Blocked: ${pkg.blocked ? 'yes' : 'no'}`);
|
|
20
|
+
lines.push('');
|
|
21
|
+
lines.push('## Objective');
|
|
22
|
+
lines.push('');
|
|
23
|
+
lines.push(pkg.objective);
|
|
24
|
+
lines.push('');
|
|
25
|
+
lines.push('## Acceptance Criteria');
|
|
26
|
+
lines.push('');
|
|
27
|
+
if (pkg.criteria.length > 0) {
|
|
28
|
+
for (const criterion of pkg.criteria) {
|
|
29
|
+
lines.push(`- [ ] ${criterion}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
lines.push('_(No criteria found — check spec.md acceptance criteria)_');
|
|
34
|
+
}
|
|
35
|
+
lines.push('');
|
|
36
|
+
lines.push('## Files to Modify');
|
|
37
|
+
lines.push('');
|
|
38
|
+
if (pkg.filesToModify.length > 0) {
|
|
39
|
+
for (const f of pkg.filesToModify) {
|
|
40
|
+
lines.push(`- ${f}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
lines.push('_(No files identified — check spec.md ## Technical / ## Files)_');
|
|
45
|
+
}
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push('## Files to Create');
|
|
48
|
+
lines.push('');
|
|
49
|
+
if (pkg.filesToCreate.length > 0) {
|
|
50
|
+
for (const f of pkg.filesToCreate) {
|
|
51
|
+
lines.push(`- ${f}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
lines.push('_(None identified)_');
|
|
56
|
+
}
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push('## Test Files');
|
|
59
|
+
lines.push('');
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
|
|
61
|
+
const filesToTest = pkg.filesToTest ?? [];
|
|
62
|
+
if (filesToTest.length > 0) {
|
|
63
|
+
for (const f of filesToTest) {
|
|
64
|
+
lines.push(`- ${f}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
lines.push('_(None identified)_');
|
|
69
|
+
}
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push('## Ownership');
|
|
72
|
+
lines.push('');
|
|
73
|
+
if (pkg.ownership.length > 0) {
|
|
74
|
+
for (const item of pkg.ownership) {
|
|
75
|
+
lines.push(`- ${item}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
lines.push('_(Missing ownership — handoff is blocked)_');
|
|
80
|
+
}
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push('## Test Plan');
|
|
83
|
+
lines.push('');
|
|
84
|
+
if (pkg.testPlan.length > 0) {
|
|
85
|
+
for (const item of pkg.testPlan) {
|
|
86
|
+
lines.push(`- ${item}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
lines.push('_(Missing test plan — handoff is blocked)_');
|
|
91
|
+
}
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push('## Risks');
|
|
94
|
+
lines.push('');
|
|
95
|
+
if (pkg.risks.length > 0) {
|
|
96
|
+
for (const item of pkg.risks) {
|
|
97
|
+
lines.push(`- ${item}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
lines.push('_(Missing risk analysis — handoff is blocked)_');
|
|
102
|
+
}
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push('## OUT OF SCOPE');
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push('**The following must NOT be implemented:**');
|
|
107
|
+
lines.push('');
|
|
108
|
+
for (const item of pkg.outOfScope) {
|
|
109
|
+
lines.push(`- ${item}`);
|
|
110
|
+
}
|
|
111
|
+
lines.push('');
|
|
112
|
+
if (pkg.constraints.length > 0) {
|
|
113
|
+
lines.push('## Hard Constraints');
|
|
114
|
+
lines.push('');
|
|
115
|
+
for (const constraint of pkg.constraints) {
|
|
116
|
+
lines.push(`- [${constraint.type.toUpperCase()}] ${constraint.description}`);
|
|
117
|
+
if (constraint.affectedItems.length > 0) {
|
|
118
|
+
lines.push(` Affects: ${constraint.affectedItems.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
lines.push('');
|
|
122
|
+
}
|
|
123
|
+
if (pkg.warnings.length > 0) {
|
|
124
|
+
lines.push('## Warnings');
|
|
125
|
+
lines.push('');
|
|
126
|
+
for (const warning of pkg.warnings) {
|
|
127
|
+
lines.push(`- WARNING: ${warning}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push('');
|
|
130
|
+
}
|
|
131
|
+
lines.push('## Current State');
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push(pkg.currentState);
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push('## Next Action');
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push(pkg.nextAction);
|
|
138
|
+
lines.push('');
|
|
139
|
+
if (pkg.blockers.length > 0) {
|
|
140
|
+
lines.push('## Blockers');
|
|
141
|
+
lines.push('');
|
|
142
|
+
for (const blocker of pkg.blockers) {
|
|
143
|
+
lines.push(`- ${blocker}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push('');
|
|
146
|
+
}
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
export function topItems(items, limit) {
|
|
150
|
+
return items.slice(0, limit);
|
|
151
|
+
}
|
|
152
|
+
export function buildHandoffSummaryText(args) {
|
|
153
|
+
const lines = [];
|
|
154
|
+
lines.push(`# Implementation Handoff Package - ${args.pkg.specId}`);
|
|
155
|
+
lines.push('');
|
|
156
|
+
lines.push(`Readiness Score: ${String(args.pkg.readinessScore)}/100`);
|
|
157
|
+
lines.push(`Blocked: ${args.pkg.blocked ? 'yes' : 'no'}`);
|
|
158
|
+
if (args.pkg.handoffPath) {
|
|
159
|
+
lines.push(`Handoff Path: ${args.pkg.handoffPath}`);
|
|
160
|
+
}
|
|
161
|
+
if (args.pkg.contextHash) {
|
|
162
|
+
lines.push(`Context Hash: ${args.pkg.contextHash}`);
|
|
163
|
+
}
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
|
|
165
|
+
const filesToTestCount = (args.pkg.filesToTest ?? []).length;
|
|
166
|
+
lines.push(`Counts: ${String(args.pkg.criteria.length)} criteria, ${String(args.pkg.filesToModify.length)} files to modify, ${String(args.pkg.filesToCreate.length)} files to create, ${String(filesToTestCount)} files to test, ${String(args.pkg.testPlan.length)} tests, ${String(args.pkg.risks.length)} risks.`);
|
|
167
|
+
if (args.topBlockers.length > 0) {
|
|
168
|
+
lines.push(`Top blockers: ${args.topBlockers.join('; ')}`);
|
|
169
|
+
}
|
|
170
|
+
if (args.topRisks.length > 0) {
|
|
171
|
+
lines.push(`Top risks: ${args.topRisks.join('; ')}`);
|
|
172
|
+
}
|
|
173
|
+
if (args.minimalityBlocked !== null) {
|
|
174
|
+
lines.push(`Minimality: ${args.minimalityBlocked ? 'blocked' : 'pass'}`);
|
|
175
|
+
}
|
|
176
|
+
if (args.tokenWasteRiskCount !== null) {
|
|
177
|
+
lines.push(`Token Waste Autopilot risks: ${String(args.tokenWasteRiskCount)}`);
|
|
178
|
+
}
|
|
179
|
+
lines.push(`Next Action: ${args.pkg.nextAction}`);
|
|
180
|
+
lines.push('');
|
|
181
|
+
lines.push('Full handoff is available via the handoffPath artifact; it is not embedded by default.');
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
export function summarizeMinimality(report) {
|
|
185
|
+
if (report === null) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const blockingFindings = report.findings.filter((finding) => finding.blocksDone);
|
|
189
|
+
return {
|
|
190
|
+
enabled: report.enabled,
|
|
191
|
+
blocked: report.blocked,
|
|
192
|
+
findingCount: report.findings.length,
|
|
193
|
+
blockingFindingCount: blockingFindings.length,
|
|
194
|
+
blockersTop3: topItems(blockingFindings, 3).map((finding) => ({
|
|
195
|
+
ruleId: finding.ruleId,
|
|
196
|
+
target: finding.target,
|
|
197
|
+
evidence: finding.evidence,
|
|
198
|
+
})),
|
|
199
|
+
safetyExceptionCount: report.safetyExceptions.length,
|
|
200
|
+
debtEvidenceCount: report.debtEvidence.length,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export function summarizeTokenWaste(report) {
|
|
204
|
+
if (report === null) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
actionsTakenCount: report.actionsTaken.length,
|
|
209
|
+
recommendationCount: report.recommendations.length,
|
|
210
|
+
riskCount: report.risks.length,
|
|
211
|
+
risksTop3: topItems(report.risks, 3).map((risk) => ({
|
|
212
|
+
decision: risk.decision,
|
|
213
|
+
target: risk.target,
|
|
214
|
+
reason: risk.reason,
|
|
215
|
+
confidence: risk.confidence,
|
|
216
|
+
})),
|
|
217
|
+
modelEffort: {
|
|
218
|
+
effort: report.modelEffort.effort,
|
|
219
|
+
reason: report.modelEffort.reason,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export function summarizeGraphContext(graphContext) {
|
|
224
|
+
if (graphContext === null || typeof graphContext !== 'object') {
|
|
225
|
+
return { available: false };
|
|
226
|
+
}
|
|
227
|
+
const graph = graphContext;
|
|
228
|
+
return {
|
|
229
|
+
available: true,
|
|
230
|
+
nodeCount: Array.isArray(graph.nodes) ? graph.nodes.length : 0,
|
|
231
|
+
edgeCount: Array.isArray(graph.edges) ? graph.edges.length : 0,
|
|
232
|
+
summary: graph.summary,
|
|
233
|
+
tokenSavings: graph.tokenSavings,
|
|
234
|
+
freshness: graph.freshness
|
|
235
|
+
? {
|
|
236
|
+
graphPath: graph.freshness.graphPath,
|
|
237
|
+
stale: graph.freshness.stale,
|
|
238
|
+
reason: graph.freshness.reason,
|
|
239
|
+
}
|
|
240
|
+
: undefined,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
export async function buildHandoffTokenWasteReport(pkg, knowledge, spec) {
|
|
244
|
+
const { policy } = await loadTokenWastePolicy(knowledge.projectPath);
|
|
245
|
+
if (!policy.enabled) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const context = analyzeContextPreflight({
|
|
249
|
+
action: 'package_handoff',
|
|
250
|
+
policy,
|
|
251
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
|
|
252
|
+
candidates: [...pkg.filesToModify, ...pkg.filesToCreate, ...(pkg.filesToTest ?? [])].map((path) => ({
|
|
253
|
+
path,
|
|
254
|
+
reason: 'Referenced by handoff package.',
|
|
255
|
+
})),
|
|
256
|
+
spec: {
|
|
257
|
+
id: spec.id,
|
|
258
|
+
risk: spec.risk,
|
|
259
|
+
target: spec.target,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
const tools = recommendRelevantTools({
|
|
263
|
+
action: 'package_handoff',
|
|
264
|
+
intentSignals: ['handoff', spec.scope ?? '', spec.target ?? ''].filter(Boolean),
|
|
265
|
+
tools: toolsFromPolicyGroups(policy),
|
|
266
|
+
policy,
|
|
267
|
+
});
|
|
268
|
+
return buildTokenWasteReport({
|
|
269
|
+
action: 'package_handoff',
|
|
270
|
+
policy,
|
|
271
|
+
context,
|
|
272
|
+
tools,
|
|
273
|
+
spec: {
|
|
274
|
+
id: spec.id,
|
|
275
|
+
risk: spec.risk,
|
|
276
|
+
scope: spec.scope,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
export async function buildGraphContextSection(args) {
|
|
281
|
+
if (args.projectPath === undefined) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const slice = await queryProjectGraphSlice({
|
|
286
|
+
projectId: args.projectId,
|
|
287
|
+
projectPath: args.projectPath,
|
|
288
|
+
specId: args.specId,
|
|
289
|
+
});
|
|
290
|
+
if (slice === null || slice.nodes.length === 0) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
return { text: await formatProjectGraphContext(slice), structured: slice };
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
export async function buildMinimalImplementationReport(pkg, knowledge, spec, handoffText) {
|
|
300
|
+
const { policy, evidence } = await loadMinimalImplementationPolicy(knowledge.projectPath);
|
|
301
|
+
return analyzeMinimalImplementation({
|
|
302
|
+
policy,
|
|
303
|
+
specId: spec.id,
|
|
304
|
+
specRisk: spec.risk,
|
|
305
|
+
handoffText,
|
|
306
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
|
|
307
|
+
files: [...pkg.filesToModify, ...pkg.filesToCreate, ...(pkg.filesToTest ?? [])].map((path) => ({
|
|
308
|
+
path,
|
|
309
|
+
content: path,
|
|
310
|
+
})),
|
|
311
|
+
evidence,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
//# sourceMappingURL=handoff-format.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Spec, ProjectKnowledge, HandoffPackage } from '../types/index.js';
|
|
1
|
+
import type { Spec, ProjectKnowledge, HandoffPackage, HandoffVersionFence } from '../types/index.js';
|
|
2
2
|
/** Normalize one repository-relative ownership path without retaining unsafe input in diagnostics. */
|
|
3
3
|
export declare function normalizeOwnedPath(value: string): {
|
|
4
4
|
status: 'valid';
|
|
@@ -7,7 +7,7 @@ export declare function normalizeOwnedPath(value: string): {
|
|
|
7
7
|
status: 'none';
|
|
8
8
|
} | {
|
|
9
9
|
status: 'invalid';
|
|
10
|
-
code: 'INVALID_FILE_OWNERSHIP_EMPTY' | 'INVALID_FILE_OWNERSHIP_FENCED' | 'INVALID_FILE_OWNERSHIP_NUL' | 'INVALID_FILE_OWNERSHIP_ABSOLUTE' | 'INVALID_FILE_OWNERSHIP_HOME_RELATIVE' | 'INVALID_FILE_OWNERSHIP_UNC' | 'INVALID_FILE_OWNERSHIP_DRIVE_ABSOLUTE' | 'INVALID_FILE_OWNERSHIP_URI' | 'INVALID_FILE_OWNERSHIP_TRAVERSAL' | 'INVALID_FILE_OWNERSHIP_MARKDOWN' | 'INVALID_FILE_OWNERSHIP_DESCRIPTION';
|
|
10
|
+
code: 'INVALID_FILE_OWNERSHIP_EMPTY' | 'INVALID_FILE_OWNERSHIP_FENCED' | 'INVALID_FILE_OWNERSHIP_NUL' | 'INVALID_FILE_OWNERSHIP_ABSOLUTE' | 'INVALID_FILE_OWNERSHIP_HOME_RELATIVE' | 'INVALID_FILE_OWNERSHIP_UNC' | 'INVALID_FILE_OWNERSHIP_DRIVE_ABSOLUTE' | 'INVALID_FILE_OWNERSHIP_URI' | 'INVALID_FILE_OWNERSHIP_TRAVERSAL' | 'INVALID_FILE_OWNERSHIP_MARKDOWN' | 'INVALID_FILE_OWNERSHIP_DESCRIPTION' | 'INVALID_FILE_OWNERSHIP_CHARSET';
|
|
11
11
|
};
|
|
12
12
|
/** Normalize a raw traceability changed-file value without interpreting Markdown or prose. */
|
|
13
13
|
export declare function normalizeChangedFilePath(value: string): ReturnType<typeof normalizeOwnedPath>;
|
|
@@ -21,5 +21,7 @@ export declare function extractCanonicalFileOwnership(content: string): {
|
|
|
21
21
|
toTestDeclared: string[];
|
|
22
22
|
blockers: string[];
|
|
23
23
|
};
|
|
24
|
-
export declare function packageHandoff(spec: Spec, knowledge: ProjectKnowledge): Promise<HandoffPackage
|
|
24
|
+
export declare function packageHandoff(spec: Spec, knowledge: ProjectKnowledge, versionFence?: HandoffVersionFence): Promise<HandoffPackage & {
|
|
25
|
+
readonly staleVersionSkipped?: boolean;
|
|
26
|
+
}>;
|
|
25
27
|
//# sourceMappingURL=handoff-packager.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/handoff-packager.ts — Assembles handoff context package for AI agents (SPEC-039)
|
|
2
|
-
|
|
3
|
-
import {
|
|
2
|
+
/* eslint-disable max-lines -- package assembly and the SPEC-1240 atomic-publish/version-fence path stay co-located for auditability */
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { join } from 'node:path';
|
|
5
6
|
import { stripFrontmatter } from './frontmatter-parser.js';
|
|
6
7
|
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
@@ -118,6 +119,7 @@ function ownershipItems(content) {
|
|
|
118
119
|
}
|
|
119
120
|
return { items: extractListItems(visibleLines.join('\n')), fenced };
|
|
120
121
|
}
|
|
122
|
+
const OWNERSHIP_DESCRIPTION_SEPARATOR = /\s+(?:—|–|-)\s+/u;
|
|
121
123
|
/** Normalize one repository-relative ownership path without retaining unsafe input in diagnostics. */
|
|
122
124
|
export function normalizeOwnedPath(value) {
|
|
123
125
|
const trimmed = value.trim();
|
|
@@ -125,7 +127,9 @@ export function normalizeOwnedPath(value) {
|
|
|
125
127
|
return { status: 'none' };
|
|
126
128
|
}
|
|
127
129
|
const backticked = /^`([^`]+)`/.exec(trimmed)?.[1];
|
|
128
|
-
const candidate = (backticked ??
|
|
130
|
+
const candidate = (backticked ??
|
|
131
|
+
trimmed.split(new RegExp(`${OWNERSHIP_DESCRIPTION_SEPARATOR.source}|\\s+\\(`, 'u'), 1)[0] ??
|
|
132
|
+
'').trim();
|
|
129
133
|
if (candidate.length === 0) {
|
|
130
134
|
return { status: 'invalid', code: 'INVALID_FILE_OWNERSHIP_EMPTY' };
|
|
131
135
|
}
|
|
@@ -323,34 +327,63 @@ function stripInertOwnershipMarkup(content) {
|
|
|
323
327
|
function normalizeStrictOwnedPath(rawValue) {
|
|
324
328
|
const normalized = normalizeOwnedPath(rawValue);
|
|
325
329
|
if (normalized.status !== 'valid') {
|
|
326
|
-
return
|
|
330
|
+
return normalized;
|
|
327
331
|
}
|
|
328
|
-
return /^[A-Za-z0-9._/()[\]@-]+$/u.test(normalized.path)
|
|
332
|
+
return /^[A-Za-z0-9._/()[\]@-]+$/u.test(normalized.path)
|
|
333
|
+
? normalized
|
|
334
|
+
: { status: 'invalid', code: 'INVALID_FILE_OWNERSHIP_CHARSET' };
|
|
335
|
+
}
|
|
336
|
+
function ownershipHeadEnd(rawPath) {
|
|
337
|
+
const masked = rawPath.replaceAll(/`[^`\n]+`/gu, (match) => 'x'.repeat(match.length));
|
|
338
|
+
return OWNERSHIP_DESCRIPTION_SEPARATOR.exec(masked)?.index ?? rawPath.length;
|
|
339
|
+
}
|
|
340
|
+
const ownershipReasonCode = (result) => result.status === 'invalid' ? result.code : 'INVALID_FILE_OWNERSHIP_EMPTY';
|
|
341
|
+
function ownershipInvalidTokenBlocker(token, classification, reasonCode) {
|
|
342
|
+
const printed = JSON.stringify(token.length > 120 ? token.slice(0, 120) : token);
|
|
343
|
+
const hint = `${OWNERSHIP_LINE_SHAPE_HINT} reason=${reasonCode}`;
|
|
344
|
+
return ownershipBlocker(printed, [classification, 'InvalidPath'], hint);
|
|
329
345
|
}
|
|
330
346
|
function collectStrictOwnershipEntry(rawValue, classification, state) {
|
|
331
347
|
const rawPath = rawValue.trim();
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
348
|
+
const head = rawPath.slice(0, ownershipHeadEnd(rawPath));
|
|
349
|
+
const backtickMatches = [...head.matchAll(/`([^`\n]+)`/gu)].map((match) => match[1] ?? '');
|
|
350
|
+
const pathCandidates = backtickMatches.filter((token) => /[./]/u.test(token));
|
|
351
|
+
const pushInvalid = (token, code) => {
|
|
352
|
+
state.blockers.push(ownershipInvalidTokenBlocker(token, classification, code));
|
|
353
|
+
};
|
|
354
|
+
if (!/`[^`\n]+`/u.test(rawPath)) {
|
|
355
|
+
const direct = normalizeStrictOwnedPath(head);
|
|
356
|
+
if (direct.status !== 'valid') {
|
|
357
|
+
pushInvalid(head, ownershipReasonCode(direct));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
collectStrictOwnershipPath(direct.path, classification, state);
|
|
339
361
|
return;
|
|
340
362
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
363
|
+
if (backtickMatches.length === 0) {
|
|
364
|
+
pushInvalid(head, 'INVALID_FILE_OWNERSHIP_HEAD_RESIDUAL');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const residual = head.replaceAll(/`[^`\n]+`/gu, '').trim();
|
|
368
|
+
if (!isSafeOwnershipResidual(residual)) {
|
|
369
|
+
pushInvalid(residual, 'INVALID_FILE_OWNERSHIP_HEAD_RESIDUAL');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const normalizedCandidates = pathCandidates.map((token) => ({
|
|
373
|
+
token,
|
|
374
|
+
result: normalizeStrictOwnedPath(token),
|
|
375
|
+
}));
|
|
376
|
+
const failed = normalizedCandidates.filter(({ result }) => result.status !== 'valid');
|
|
377
|
+
if (failed.length > 0) {
|
|
378
|
+
for (const { token, result } of failed) {
|
|
379
|
+
pushInvalid(token, ownershipReasonCode(result));
|
|
380
|
+
}
|
|
350
381
|
return;
|
|
351
382
|
}
|
|
352
|
-
for (const
|
|
353
|
-
|
|
383
|
+
for (const { result } of normalizedCandidates) {
|
|
384
|
+
if (result.status === 'valid') {
|
|
385
|
+
collectStrictOwnershipPath(result.path, classification, state);
|
|
386
|
+
}
|
|
354
387
|
}
|
|
355
388
|
}
|
|
356
389
|
function collectStrictOwnershipPath(path, classification, state) {
|
|
@@ -435,7 +468,7 @@ function extractFileLevelWorkPlanOwnership(content) {
|
|
|
435
468
|
};
|
|
436
469
|
}
|
|
437
470
|
function validateOwnershipAgreement(canonical, workPlan, testDeclaredPaths) {
|
|
438
|
-
if (!canonical.present
|
|
471
|
+
if (!canonical.present) {
|
|
439
472
|
return [];
|
|
440
473
|
}
|
|
441
474
|
const testDeclared = new Set(testDeclaredPaths);
|
|
@@ -444,6 +477,13 @@ function validateOwnershipAgreement(canonical, workPlan, testDeclaredPaths) {
|
|
|
444
477
|
: ownership.toModify.includes(path)
|
|
445
478
|
? 'Modify'
|
|
446
479
|
: 'Absent';
|
|
480
|
+
if (!workPlan.present) {
|
|
481
|
+
const canonicalPaths = [...canonical.toCreate, ...canonical.toModify].filter((path) => !testDeclared.has(path));
|
|
482
|
+
return [...new Set(canonicalPaths)].sort().map((path) => {
|
|
483
|
+
const canonicalClass = classification(canonical, path);
|
|
484
|
+
return ownershipBlocker(path, [`Canonical:${canonicalClass}`, 'WorkPlan:Absent'], ownershipAgreementHint(canonicalClass, 'Absent', path));
|
|
485
|
+
});
|
|
486
|
+
}
|
|
447
487
|
const paths = new Set([
|
|
448
488
|
...canonical.toCreate,
|
|
449
489
|
...canonical.toModify,
|
|
@@ -463,9 +503,9 @@ function validateOwnershipAgreement(canonical, workPlan, testDeclaredPaths) {
|
|
|
463
503
|
function extractBacktickedFiles(content) {
|
|
464
504
|
const files = new Set();
|
|
465
505
|
for (const match of content.matchAll(/`([^`\n]+)`/gu)) {
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
468
|
-
files.add(path);
|
|
506
|
+
const normalized = match[1] ? normalizeStrictOwnedPath(match[1]) : null;
|
|
507
|
+
if (normalized?.status === 'valid') {
|
|
508
|
+
files.add(normalized.path);
|
|
469
509
|
}
|
|
470
510
|
}
|
|
471
511
|
return [...files].sort();
|
|
@@ -602,7 +642,7 @@ function buildConstraints(knowledge) {
|
|
|
602
642
|
return constraints;
|
|
603
643
|
}
|
|
604
644
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
605
|
-
export async function packageHandoff(spec, knowledge) {
|
|
645
|
+
export async function packageHandoff(spec, knowledge, versionFence) {
|
|
606
646
|
const warnings = [];
|
|
607
647
|
const rawSpecContent = spec.specPath
|
|
608
648
|
? await readFile(spec.specPath, 'utf-8').catch(() => '')
|
|
@@ -685,7 +725,7 @@ export async function packageHandoff(spec, knowledge) {
|
|
|
685
725
|
blocked: blockers.length > 0,
|
|
686
726
|
blockers,
|
|
687
727
|
};
|
|
688
|
-
return persistHandoffIfPossible(pkg, knowledge);
|
|
728
|
+
return persistHandoffIfPossible(pkg, knowledge, versionFence);
|
|
689
729
|
}
|
|
690
730
|
function renderPersistedHandoff(pkg) {
|
|
691
731
|
return [
|
|
@@ -733,19 +773,36 @@ function renderPersistedHandoff(pkg) {
|
|
|
733
773
|
'',
|
|
734
774
|
].join('\n');
|
|
735
775
|
}
|
|
736
|
-
async function persistHandoffIfPossible(pkg, knowledge) {
|
|
776
|
+
async function persistHandoffIfPossible(pkg, knowledge, versionFence) {
|
|
737
777
|
if (!knowledge.projectPath) {
|
|
738
778
|
return pkg;
|
|
739
779
|
}
|
|
740
780
|
const handoffDir = join(projectDataDir(hashProjectPath(knowledge.projectPath)), 'handoffs');
|
|
741
781
|
const handoffPath = join(handoffDir, `${pkg.specId}.md`);
|
|
782
|
+
const tempPath = join(handoffDir, `.${pkg.specId}.md.tmp-${randomUUID()}`);
|
|
742
783
|
const sessionContextPath = join(knowledge.projectPath, 'planu', 'session-context.md');
|
|
743
784
|
const markdown = renderPersistedHandoff(pkg);
|
|
744
785
|
try {
|
|
745
786
|
await mkdir(handoffDir, { recursive: true });
|
|
746
|
-
await writeFile(
|
|
787
|
+
await writeFile(tempPath, markdown, 'utf-8');
|
|
788
|
+
if (versionFence) {
|
|
789
|
+
const abandoned = versionFence.signal?.aborted === true;
|
|
790
|
+
const currentSpecVersion = abandoned ? undefined : await versionFence.getCurrentSpecVersion();
|
|
791
|
+
if (abandoned ||
|
|
792
|
+
(currentSpecVersion !== undefined && currentSpecVersion !== versionFence.specVersion)) {
|
|
793
|
+
await rm(tempPath, { force: true });
|
|
794
|
+
return { ...pkg, staleVersionSkipped: true };
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
await rename(tempPath, handoffPath);
|
|
747
798
|
}
|
|
748
|
-
catch {
|
|
799
|
+
catch (error) {
|
|
800
|
+
await rm(tempPath, { force: true }).catch(() => {
|
|
801
|
+
/* best-effort temp cleanup — the publish already failed */
|
|
802
|
+
});
|
|
803
|
+
if (versionFence) {
|
|
804
|
+
throw error;
|
|
805
|
+
}
|
|
749
806
|
return {
|
|
750
807
|
...pkg,
|
|
751
808
|
warnings: [
|