@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { stat } from 'node:fs/promises';
|
|
2
1
|
import { isAbsolute, join, normalize } from 'node:path';
|
|
2
|
+
import { pathExistsByStat as pathExists } from '../../core/shared/fs.js';
|
|
3
3
|
import { createCriterionIdentity, criterionIdentitiesMatch } from '../criterion-identity.js';
|
|
4
4
|
export function criterionKey(criterion) {
|
|
5
5
|
return createCriterionIdentity(criterion).stableKey;
|
|
@@ -9,6 +9,10 @@ function truncateAtEvidenceMarker(value) {
|
|
|
9
9
|
const markerIndex = value.search(EVIDENCE_MARKER);
|
|
10
10
|
return markerIndex === -1 ? value : value.slice(0, markerIndex);
|
|
11
11
|
}
|
|
12
|
+
const LINE_CITATION_SUFFIX = /:\d+(?:-\d+)?$/;
|
|
13
|
+
function pathForExistenceCheck(value) {
|
|
14
|
+
return value.replace(LINE_CITATION_SUFFIX, '');
|
|
15
|
+
}
|
|
12
16
|
export async function buildSpecEvidenceIndex(args) {
|
|
13
17
|
const diagnostics = [...args.artifacts.invalidArtifacts];
|
|
14
18
|
const matrixRows = args.artifacts.traceabilityMatrix?.rows ?? [];
|
|
@@ -129,12 +133,25 @@ async function pathRecord(kind, value, source, args, fieldName) {
|
|
|
129
133
|
if (!args.projectPath) {
|
|
130
134
|
return record(kind, value, 'missing', source, evidencePathReason(fieldName, 'projectPath unavailable'));
|
|
131
135
|
}
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
.catch(() => false);
|
|
136
|
+
const literalExists = await pathExists(safe.path);
|
|
137
|
+
const exists = literalExists || (await fallbackExists(value, fieldName, args.projectPath));
|
|
135
138
|
const missingReason = evidencePathReason(fieldName, `Missing path: ${value}`);
|
|
136
139
|
return record(kind, value, exists ? 'valid' : missingPathStatus(fieldName), source, exists ? undefined : missingReason);
|
|
137
140
|
}
|
|
141
|
+
async function fallbackExists(value, fieldName, projectPath) {
|
|
142
|
+
if (fieldName === 'changedFiles') {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const strippedValue = pathForExistenceCheck(value);
|
|
146
|
+
if (strippedValue === value || strippedValue.length === 0) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const strippedSafe = resolveSafeProjectPath(strippedValue, projectPath, fieldName);
|
|
150
|
+
if (!strippedSafe.ok) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return pathExists(strippedSafe.path);
|
|
154
|
+
}
|
|
138
155
|
function missingPathStatus(fieldName) {
|
|
139
156
|
return fieldName === 'changedFiles' ? 'missing' : 'stale';
|
|
140
157
|
}
|
|
@@ -249,7 +249,7 @@ function isQueueRelevant(job) {
|
|
|
249
249
|
return true;
|
|
250
250
|
}
|
|
251
251
|
const kind = job.payload.kind;
|
|
252
|
-
return kind === undefined || kind === 'validate';
|
|
252
|
+
return kind === undefined || kind === 'validate' || kind === 'package-handoff';
|
|
253
253
|
}
|
|
254
254
|
function remainingBudgetMs(job, now = Date.now()) {
|
|
255
255
|
const payload = job.payload && typeof job.payload === 'object'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DurableJobRuntime } from './job-runtime.js';
|
|
2
|
+
import type { DurableJobRecord, PackageHandoffDurableJobPayload, PackageHandoffStageResult } from '../../types/durable-job.js';
|
|
3
|
+
export declare function isPackageHandoffJob(value: unknown): boolean;
|
|
4
|
+
export declare function parsePackageHandoffPayload(value: unknown): PackageHandoffDurableJobPayload | undefined;
|
|
5
|
+
/** Bounds one stage with AbortSignal.timeout + Promise.race; the stage's own promise is orphaned on timeout. */
|
|
6
|
+
export declare function runStageWithDeadline<T>(stage: string, budgetMs: number, fn: (signal: AbortSignal) => Promise<T>): Promise<PackageHandoffStageResult<T>>;
|
|
7
|
+
/** Executes accepted package-handoff jobs directly against the packaging engine, never through MCP. */
|
|
8
|
+
export declare class PackageHandoffJobExecutor {
|
|
9
|
+
private readonly runtime;
|
|
10
|
+
private readonly workerId;
|
|
11
|
+
constructor(runtime: DurableJobRuntime, workerId?: string);
|
|
12
|
+
runOnce(projectId: string, workspaceId?: string): Promise<readonly DurableJobRecord[]>;
|
|
13
|
+
runUntilIdle(projectId: string, workspaceId?: string): Promise<void>;
|
|
14
|
+
private candidates;
|
|
15
|
+
private execute;
|
|
16
|
+
private runJob;
|
|
17
|
+
}
|
|
18
|
+
/** Schedules background execution after the submit response can be flushed. */
|
|
19
|
+
export declare function schedulePackageHandoffJobExecution(projectId: string, workspaceId?: string): void;
|
|
20
|
+
/** Re-enqueues accepted/checkpointed package-handoff work discovered after a process restart. */
|
|
21
|
+
export declare function recoverPackageHandoffJobsAtStartup(): void;
|
|
22
|
+
//# sourceMappingURL=package-handoff-job-executor.d.ts.map
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
// engine/execution/package-handoff-job-executor.ts — Durable executor for package_handoff (SPEC-1240)
|
|
2
|
+
// Mirrors the validate-job-executor submit/claim/execute lifecycle, bounding every stage with
|
|
3
|
+
// its own deadline so a slow readiness check, packager write, or enrichment stage can never hang
|
|
4
|
+
// the MCP transport. Core stages fail the job classified; enrichment stages degrade and the job
|
|
5
|
+
// still completes.
|
|
6
|
+
/* eslint-disable max-lines -- stage deadlines, core/enrichment orchestration, and the durable executor lifecycle stay co-located for auditability */
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { DurableJobRuntime } from './job-runtime.js';
|
|
9
|
+
import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
|
|
10
|
+
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
11
|
+
import { specStore, knowledgeStore } from '../../storage/index.js';
|
|
12
|
+
import { checkSpecReadiness } from '../readiness-checker.js';
|
|
13
|
+
import { packageHandoff } from '../handoff-packager.js';
|
|
14
|
+
import { detectParadigms } from '../paradigm-detector.js';
|
|
15
|
+
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
16
|
+
import { appendTransitionEvent } from '../../storage/transition-log.js';
|
|
17
|
+
import { formatHandoff, topItems, buildHandoffSummaryText, summarizeMinimality, summarizeTokenWaste, summarizeGraphContext, buildHandoffTokenWasteReport, buildGraphContextSection, buildMinimalImplementationReport, } from '../handoff-format.js';
|
|
18
|
+
const WORKSPACE_ID = 'global';
|
|
19
|
+
const CORE_BUDGET_DEFAULT_MS = 20_000;
|
|
20
|
+
const ENRICHMENT_BUDGET_DEFAULT_MS = 8_000;
|
|
21
|
+
const ENRICHMENT_STAGE_COUNT = 4;
|
|
22
|
+
const LEASE_SAFETY_MARGIN_MS = 15_000;
|
|
23
|
+
const activeScopes = new Map();
|
|
24
|
+
export function isPackageHandoffJob(value) {
|
|
25
|
+
return Boolean(value && typeof value === 'object' && value.kind === 'package-handoff');
|
|
26
|
+
}
|
|
27
|
+
export function parsePackageHandoffPayload(value) {
|
|
28
|
+
if (!value || typeof value !== 'object') {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const candidate = value;
|
|
32
|
+
if (candidate.kind === 'package-handoff' &&
|
|
33
|
+
candidate.schemaVersion === 1 &&
|
|
34
|
+
typeof candidate.projectId === 'string' &&
|
|
35
|
+
typeof candidate.specId === 'string' &&
|
|
36
|
+
typeof candidate.specVersion === 'string') {
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
function envBudgetMs(variableName, defaultMs) {
|
|
42
|
+
const raw = process.env[variableName];
|
|
43
|
+
if (raw === undefined || raw.trim() === '') {
|
|
44
|
+
return defaultMs;
|
|
45
|
+
}
|
|
46
|
+
const parsed = Number.parseInt(raw, 10);
|
|
47
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultMs;
|
|
48
|
+
}
|
|
49
|
+
function coreBudgetMs() {
|
|
50
|
+
return envBudgetMs('PLANU_HANDOFF_CORE_BUDGET_MS', CORE_BUDGET_DEFAULT_MS);
|
|
51
|
+
}
|
|
52
|
+
function enrichmentBudgetMs() {
|
|
53
|
+
return envBudgetMs('PLANU_HANDOFF_ENRICHMENT_BUDGET_MS', ENRICHMENT_BUDGET_DEFAULT_MS);
|
|
54
|
+
}
|
|
55
|
+
/** Bounds one stage with AbortSignal.timeout + Promise.race; the stage's own promise is orphaned on timeout. */
|
|
56
|
+
export async function runStageWithDeadline(stage, budgetMs, fn) {
|
|
57
|
+
const startedAt = Date.now();
|
|
58
|
+
if (budgetMs <= 0) {
|
|
59
|
+
return {
|
|
60
|
+
stage,
|
|
61
|
+
durationMs: 0,
|
|
62
|
+
outcome: {
|
|
63
|
+
ok: false,
|
|
64
|
+
timedOut: true,
|
|
65
|
+
error: new Error(`[Planu] stage "${stage}" had no remaining budget`),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const timeoutSignal = AbortSignal.timeout(budgetMs);
|
|
70
|
+
let timedOut = false;
|
|
71
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
72
|
+
timeoutSignal.addEventListener('abort', () => {
|
|
73
|
+
timedOut = true;
|
|
74
|
+
reject(new Error(`[Planu] stage "${stage}" exceeded its ${String(budgetMs)}ms budget`));
|
|
75
|
+
}, { once: true });
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
const value = await Promise.race([fn(timeoutSignal), timeoutPromise]);
|
|
79
|
+
return {
|
|
80
|
+
stage,
|
|
81
|
+
durationMs: Date.now() - startedAt,
|
|
82
|
+
outcome: { ok: true, value, timedOut: false },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
reportClassifiedDegradation('HANDOFF_STAGE_FAILURE', error);
|
|
87
|
+
return { stage, durationMs: Date.now() - startedAt, outcome: { ok: false, timedOut, error } };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
class CoreStageFailure extends Error {
|
|
91
|
+
stage;
|
|
92
|
+
timedOut;
|
|
93
|
+
cause;
|
|
94
|
+
stageTimings;
|
|
95
|
+
constructor(stage, timedOut, cause, stageTimings) {
|
|
96
|
+
super(`[Planu] package-handoff core stage "${stage}" failed`);
|
|
97
|
+
this.stage = stage;
|
|
98
|
+
this.timedOut = timedOut;
|
|
99
|
+
this.cause = cause;
|
|
100
|
+
this.stageTimings = stageTimings;
|
|
101
|
+
this.name = 'CoreStageFailure';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function classifiedCoreFailureMessage(failure) {
|
|
105
|
+
const code = failure.timedOut ? 'HANDOFF_CORE_TIMEOUT' : 'HANDOFF_CORE_FAILURE';
|
|
106
|
+
const remediation = failure.timedOut
|
|
107
|
+
? `The ${failure.stage} stage exceeded its budget. Increase PLANU_HANDOFF_CORE_BUDGET_MS or investigate why ${failure.stage} is slow, then retry with restart_job.`
|
|
108
|
+
: `The ${failure.stage} stage failed. Inspect the underlying cause, then retry with restart_job once fixed.`;
|
|
109
|
+
const cause = failure.cause instanceof Error ? failure.cause.message : String(failure.cause);
|
|
110
|
+
return (`[Planu] ${code} stage=${failure.stage} remediation=${remediation} cause=${cause} ` +
|
|
111
|
+
`stageTimings=${JSON.stringify(failure.stageTimings)}`);
|
|
112
|
+
}
|
|
113
|
+
function enrichmentDegradation(stage, timedOut) {
|
|
114
|
+
return {
|
|
115
|
+
stage,
|
|
116
|
+
code: timedOut ? 'HANDOFF_ENRICHMENT_TIMEOUT' : 'HANDOFF_ENRICHMENT_FAILURE',
|
|
117
|
+
remediation: timedOut
|
|
118
|
+
? `The ${stage} enrichment stage exceeded its budget; the core handoff artifact is still valid. Increase PLANU_HANDOFF_ENRICHMENT_BUDGET_MS to include it next run.`
|
|
119
|
+
: `The ${stage} enrichment stage failed; the core handoff artifact is still valid. Re-run package_handoff once the underlying issue is fixed.`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function currentSpecVersion(projectId, specId, projectPath) {
|
|
123
|
+
const current = await specStore.getSpec(projectId, specId, projectPath);
|
|
124
|
+
return current?.spec_version;
|
|
125
|
+
}
|
|
126
|
+
async function runCoreStages(payload, spec, knowledge) {
|
|
127
|
+
const deadlineAt = Date.now() + coreBudgetMs();
|
|
128
|
+
const stageTimings = {};
|
|
129
|
+
const readinessResult = await runStageWithDeadline('readiness', Math.max(0, deadlineAt - Date.now()), () => checkSpecReadiness(spec, 'lenient'));
|
|
130
|
+
stageTimings.readiness = readinessResult.durationMs;
|
|
131
|
+
if (!readinessResult.outcome.ok) {
|
|
132
|
+
throw new CoreStageFailure('readiness', readinessResult.outcome.timedOut, readinessResult.outcome.error, stageTimings);
|
|
133
|
+
}
|
|
134
|
+
const packageCoreResult = await runStageWithDeadline('package-core', Math.max(0, deadlineAt - Date.now()), (signal) => packageHandoff(spec, knowledge, {
|
|
135
|
+
specVersion: payload.specVersion,
|
|
136
|
+
getCurrentSpecVersion: () => currentSpecVersion(payload.projectId, payload.specId, knowledge.projectPath),
|
|
137
|
+
signal,
|
|
138
|
+
}));
|
|
139
|
+
stageTimings['package-core'] = packageCoreResult.durationMs;
|
|
140
|
+
if (!packageCoreResult.outcome.ok) {
|
|
141
|
+
throw new CoreStageFailure('package-core', packageCoreResult.outcome.timedOut, packageCoreResult.outcome.error, stageTimings);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
readinessScore: readinessResult.outcome.value?.score ?? 0,
|
|
145
|
+
pkg: packageCoreResult.outcome.value,
|
|
146
|
+
stageTimings,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function runParadigmStage(knowledge, constraints) {
|
|
150
|
+
if (!knowledge.projectPath) {
|
|
151
|
+
return { durationMs: 0, constraints, degradation: undefined };
|
|
152
|
+
}
|
|
153
|
+
const projectPath = knowledge.projectPath;
|
|
154
|
+
const paradigmResult = await runStageWithDeadline('paradigms', enrichmentBudgetMs(), () => detectParadigms(projectPath));
|
|
155
|
+
if (!paradigmResult.outcome.ok) {
|
|
156
|
+
return {
|
|
157
|
+
durationMs: paradigmResult.durationMs,
|
|
158
|
+
constraints,
|
|
159
|
+
degradation: enrichmentDegradation('paradigms', paradigmResult.outcome.timedOut),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const dominant = paradigmResult.outcome.value?.dominant;
|
|
163
|
+
if (!dominant || dominant.confidence < 0.5) {
|
|
164
|
+
return { durationMs: paradigmResult.durationMs, constraints, degradation: undefined };
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
durationMs: paradigmResult.durationMs,
|
|
168
|
+
constraints: [
|
|
169
|
+
...constraints,
|
|
170
|
+
{
|
|
171
|
+
type: 'interface',
|
|
172
|
+
description: `Follow ${dominant.name} paradigm patterns — dominant paradigm (confidence: ${(dominant.confidence * 100).toFixed(0)}%)`,
|
|
173
|
+
affectedItems: ['all new code'],
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
degradation: undefined,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async function runGraphContextStage(payload, knowledge) {
|
|
180
|
+
const graphResult = await runStageWithDeadline('graph-context', enrichmentBudgetMs(), () => buildGraphContextSection({
|
|
181
|
+
projectId: payload.projectId,
|
|
182
|
+
projectPath: knowledge.projectPath,
|
|
183
|
+
specId: payload.specId,
|
|
184
|
+
}));
|
|
185
|
+
if (!graphResult.outcome.ok) {
|
|
186
|
+
return {
|
|
187
|
+
durationMs: graphResult.durationMs,
|
|
188
|
+
graphContext: null,
|
|
189
|
+
degradation: enrichmentDegradation('graph-context', graphResult.outcome.timedOut),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const graphContext = graphResult.outcome.value ?? null;
|
|
193
|
+
const unavailable = graphContext === null;
|
|
194
|
+
return {
|
|
195
|
+
durationMs: graphResult.durationMs,
|
|
196
|
+
graphContext,
|
|
197
|
+
degradation: unavailable
|
|
198
|
+
? {
|
|
199
|
+
stage: 'graph-context',
|
|
200
|
+
code: 'HANDOFF_ENRICHMENT_FAILURE',
|
|
201
|
+
remediation: 'The project graph had no data for this spec — not a failure. Run configure_code_graph or index the project to include graph context in future handoffs.',
|
|
202
|
+
}
|
|
203
|
+
: undefined,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
async function runEnrichmentStages(payload, spec, knowledge, pkgWithScore) {
|
|
207
|
+
const stageTimings = {};
|
|
208
|
+
const degradations = [];
|
|
209
|
+
const budgetMs = enrichmentBudgetMs();
|
|
210
|
+
const paradigmStage = await runParadigmStage(knowledge, pkgWithScore.constraints);
|
|
211
|
+
stageTimings.paradigms = paradigmStage.durationMs;
|
|
212
|
+
if (paradigmStage.degradation) {
|
|
213
|
+
degradations.push(paradigmStage.degradation);
|
|
214
|
+
}
|
|
215
|
+
const pkgWithConstraints = {
|
|
216
|
+
...pkgWithScore,
|
|
217
|
+
constraints: paradigmStage.constraints,
|
|
218
|
+
};
|
|
219
|
+
const tokenWasteResult = await runStageWithDeadline('token-waste', budgetMs, () => buildHandoffTokenWasteReport(pkgWithConstraints, knowledge, spec));
|
|
220
|
+
stageTimings['token-waste'] = tokenWasteResult.durationMs;
|
|
221
|
+
const tokenWasteReport = tokenWasteResult.outcome.ok
|
|
222
|
+
? (tokenWasteResult.outcome.value ?? null)
|
|
223
|
+
: null;
|
|
224
|
+
if (!tokenWasteResult.outcome.ok) {
|
|
225
|
+
degradations.push(enrichmentDegradation('token-waste', tokenWasteResult.outcome.timedOut));
|
|
226
|
+
}
|
|
227
|
+
const graphStage = await runGraphContextStage(payload, knowledge);
|
|
228
|
+
stageTimings['graph-context'] = graphStage.durationMs;
|
|
229
|
+
if (graphStage.degradation) {
|
|
230
|
+
degradations.push(graphStage.degradation);
|
|
231
|
+
}
|
|
232
|
+
const graphContext = graphStage.graphContext;
|
|
233
|
+
const baseHandoffText = graphContext !== null
|
|
234
|
+
? `${formatHandoff(pkgWithConstraints)}\n${graphContext.text}`
|
|
235
|
+
: formatHandoff(pkgWithConstraints);
|
|
236
|
+
const minimalityResult = await runStageWithDeadline('minimality', budgetMs, () => buildMinimalImplementationReport(pkgWithConstraints, knowledge, spec, baseHandoffText));
|
|
237
|
+
stageTimings.minimality = minimalityResult.durationMs;
|
|
238
|
+
const minimalityReport = minimalityResult.outcome.ok
|
|
239
|
+
? (minimalityResult.outcome.value ?? null)
|
|
240
|
+
: null;
|
|
241
|
+
if (!minimalityResult.outcome.ok) {
|
|
242
|
+
degradations.push(enrichmentDegradation('minimality', minimalityResult.outcome.timedOut));
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
pkgWithConstraints,
|
|
246
|
+
graphContext,
|
|
247
|
+
minimalityBlocked: minimalityReport?.blocked ?? null,
|
|
248
|
+
minimalitySummary: summarizeMinimality(minimalityReport),
|
|
249
|
+
tokenWasteRiskCount: tokenWasteReport?.risks.length ?? null,
|
|
250
|
+
tokenWasteSummary: summarizeTokenWaste(tokenWasteReport),
|
|
251
|
+
stageTimings,
|
|
252
|
+
degradations,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function buildCompletionResult(args) {
|
|
256
|
+
const { specId, pkg, enrichment, stageTimings, degradations } = args;
|
|
257
|
+
const topBlockers = topItems(pkg.blockers, 3);
|
|
258
|
+
const ownershipBlockers = pkg.blockers.filter((blocker) => blocker.startsWith('handoff_ownership_invalid'));
|
|
259
|
+
const topRisks = topItems(pkg.risks, 5);
|
|
260
|
+
const graphSummary = enrichment.graphContext !== null
|
|
261
|
+
? summarizeGraphContext(enrichment.graphContext.structured)
|
|
262
|
+
: undefined;
|
|
263
|
+
return {
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: 'text',
|
|
267
|
+
text: buildHandoffSummaryText({
|
|
268
|
+
pkg,
|
|
269
|
+
topBlockers,
|
|
270
|
+
topRisks,
|
|
271
|
+
minimalityBlocked: enrichment.minimalityBlocked,
|
|
272
|
+
tokenWasteRiskCount: enrichment.tokenWasteRiskCount,
|
|
273
|
+
}),
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
structuredContent: {
|
|
277
|
+
resultSchema: 'package-handoff-completion/v1',
|
|
278
|
+
specId,
|
|
279
|
+
blocked: pkg.blocked,
|
|
280
|
+
readinessScore: pkg.readinessScore,
|
|
281
|
+
blockersCount: pkg.blockers.length,
|
|
282
|
+
blockersTop3: topBlockers,
|
|
283
|
+
ownershipBlockers,
|
|
284
|
+
risksTop5: topRisks,
|
|
285
|
+
criteriaTop5: topItems(pkg.criteria, 5),
|
|
286
|
+
constraintsTop5: topItems(pkg.constraints, 5),
|
|
287
|
+
warningsTop5: topItems(pkg.warnings, 5),
|
|
288
|
+
handoffPath: pkg.handoffPath,
|
|
289
|
+
contextHash: pkg.contextHash,
|
|
290
|
+
artifactRefs: { handoff: pkg.handoffPath },
|
|
291
|
+
counts: {
|
|
292
|
+
criteria: pkg.criteria.length,
|
|
293
|
+
filesToModify: pkg.filesToModify.length,
|
|
294
|
+
filesToCreate: pkg.filesToCreate.length,
|
|
295
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
|
|
296
|
+
filesToTest: (pkg.filesToTest ?? []).length,
|
|
297
|
+
ownership: pkg.ownership.length,
|
|
298
|
+
testPlan: pkg.testPlan.length,
|
|
299
|
+
risks: pkg.risks.length,
|
|
300
|
+
constraints: pkg.constraints.length,
|
|
301
|
+
warnings: pkg.warnings.length,
|
|
302
|
+
outOfScope: pkg.outOfScope.length,
|
|
303
|
+
},
|
|
304
|
+
nextAction: pkg.nextAction,
|
|
305
|
+
...(enrichment.minimalitySummary !== undefined
|
|
306
|
+
? { minimality: enrichment.minimalitySummary }
|
|
307
|
+
: {}),
|
|
308
|
+
...(enrichment.tokenWasteSummary !== undefined
|
|
309
|
+
? { tokenWaste: enrichment.tokenWasteSummary }
|
|
310
|
+
: {}),
|
|
311
|
+
...(graphSummary !== undefined ? { graphContext: graphSummary } : {}),
|
|
312
|
+
stageTimings,
|
|
313
|
+
degradations,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** Executes accepted package-handoff jobs directly against the packaging engine, never through MCP. */
|
|
318
|
+
export class PackageHandoffJobExecutor {
|
|
319
|
+
runtime;
|
|
320
|
+
workerId;
|
|
321
|
+
constructor(runtime, workerId = `handoff-${randomUUID()}`) {
|
|
322
|
+
this.runtime = runtime;
|
|
323
|
+
this.workerId = workerId;
|
|
324
|
+
}
|
|
325
|
+
async runOnce(projectId, workspaceId = WORKSPACE_ID) {
|
|
326
|
+
const candidates = this.candidates(projectId, workspaceId);
|
|
327
|
+
return Promise.all(candidates.map((job) => this.execute(job)));
|
|
328
|
+
}
|
|
329
|
+
async runUntilIdle(projectId, workspaceId = WORKSPACE_ID) {
|
|
330
|
+
for (;;) {
|
|
331
|
+
const candidates = this.candidates(projectId, workspaceId);
|
|
332
|
+
if (candidates.length === 0) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
await Promise.all(candidates.map((job) => this.execute(job).catch((error) => {
|
|
336
|
+
reportClassifiedDegradation('HANDOFF_JOB_EXECUTION_FAILURE', error);
|
|
337
|
+
})));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
candidates(projectId, workspaceId) {
|
|
341
|
+
return this.runtime
|
|
342
|
+
.list(projectId, workspaceId)
|
|
343
|
+
.filter((job) => (job.state === 'accepted' || job.state === 'checkpointed') &&
|
|
344
|
+
isPackageHandoffJob(job.payload));
|
|
345
|
+
}
|
|
346
|
+
async execute(job) {
|
|
347
|
+
const identity = {
|
|
348
|
+
operationId: job.id,
|
|
349
|
+
projectId: job.projectId,
|
|
350
|
+
workspaceId: job.workspaceId,
|
|
351
|
+
};
|
|
352
|
+
const leaseMs = coreBudgetMs() + enrichmentBudgetMs() * ENRICHMENT_STAGE_COUNT + LEASE_SAFETY_MARGIN_MS;
|
|
353
|
+
const claimed = this.runtime.claim({ ...identity, workerId: this.workerId, leaseMs });
|
|
354
|
+
if (!claimed) {
|
|
355
|
+
return this.runtime.get(identity);
|
|
356
|
+
}
|
|
357
|
+
const payload = parsePackageHandoffPayload(claimed.payload);
|
|
358
|
+
if (!payload) {
|
|
359
|
+
return this.runtime.fail({
|
|
360
|
+
...identity,
|
|
361
|
+
workerId: this.workerId,
|
|
362
|
+
fencingToken: claimed.fencingToken,
|
|
363
|
+
error: '[Planu] Invalid package-handoff job payload',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
const result = await this.runJob(payload);
|
|
368
|
+
const completed = this.runtime.complete({
|
|
369
|
+
...identity,
|
|
370
|
+
workerId: this.workerId,
|
|
371
|
+
fencingToken: claimed.fencingToken,
|
|
372
|
+
result,
|
|
373
|
+
});
|
|
374
|
+
const structured = result.structuredContent;
|
|
375
|
+
try {
|
|
376
|
+
await appendTransitionEvent({
|
|
377
|
+
projectId: payload.projectId,
|
|
378
|
+
specId: payload.specId,
|
|
379
|
+
eventType: 'handoff.intake',
|
|
380
|
+
gateResults: { handoff: structured?.blocked === true ? 'fail' : 'pass' },
|
|
381
|
+
meta: {
|
|
382
|
+
handoffPath: structured?.handoffPath,
|
|
383
|
+
contextHash: structured?.contextHash,
|
|
384
|
+
blockers: structured?.blockersTop3 ?? [],
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
reportClassifiedDegradation('HANDOFF_INTAKE_EVENT_FAILURE', error);
|
|
390
|
+
}
|
|
391
|
+
return completed;
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (error instanceof CoreStageFailure) {
|
|
395
|
+
return this.runtime.fail({
|
|
396
|
+
...identity,
|
|
397
|
+
workerId: this.workerId,
|
|
398
|
+
fencingToken: claimed.fencingToken,
|
|
399
|
+
error: classifiedCoreFailureMessage(error),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
reportClassifiedDegradation('HANDOFF_JOB_EXECUTION_FAILURE', error);
|
|
403
|
+
return this.runtime.fail({
|
|
404
|
+
...identity,
|
|
405
|
+
workerId: this.workerId,
|
|
406
|
+
fencingToken: claimed.fencingToken,
|
|
407
|
+
error: (error instanceof Error ? error.message : String(error)).slice(0, 4_096),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async runJob(payload) {
|
|
412
|
+
const spec = await specStore.getSpec(payload.projectId, payload.specId);
|
|
413
|
+
if (!spec) {
|
|
414
|
+
throw new Error(`[Planu] Spec '${payload.specId}' not found in project '${payload.projectId}'`);
|
|
415
|
+
}
|
|
416
|
+
const knowledge = await knowledgeStore.getKnowledge(payload.projectId);
|
|
417
|
+
if (!knowledge) {
|
|
418
|
+
throw new Error(`[Planu] Project '${payload.projectId}' not found. Run init_project first.`);
|
|
419
|
+
}
|
|
420
|
+
const core = await runCoreStages(payload, spec, knowledge);
|
|
421
|
+
const pkgWithScore = { ...core.pkg, readinessScore: core.readinessScore };
|
|
422
|
+
const publishDegradation = core.pkg.staleVersionSkipped === true
|
|
423
|
+
? {
|
|
424
|
+
stage: 'artifact-publish',
|
|
425
|
+
code: 'HANDOFF_STALE_VERSION_SKIPPED',
|
|
426
|
+
remediation: 'The spec advanced to a newer spec_version before this job published its artifact; the newer artifact was preserved untouched. Re-run package_handoff to publish the current version.',
|
|
427
|
+
}
|
|
428
|
+
: undefined;
|
|
429
|
+
const enrichment = await runEnrichmentStages(payload, spec, knowledge, pkgWithScore);
|
|
430
|
+
return buildCompletionResult({
|
|
431
|
+
specId: payload.specId,
|
|
432
|
+
pkg: enrichment.pkgWithConstraints,
|
|
433
|
+
enrichment,
|
|
434
|
+
stageTimings: { ...core.stageTimings, ...enrichment.stageTimings },
|
|
435
|
+
degradations: publishDegradation
|
|
436
|
+
? [publishDegradation, ...enrichment.degradations]
|
|
437
|
+
: [...enrichment.degradations],
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/** Schedules background execution after the submit response can be flushed. */
|
|
442
|
+
export function schedulePackageHandoffJobExecution(projectId, workspaceId = WORKSPACE_ID) {
|
|
443
|
+
const scope = `${projectId}:${workspaceId}`;
|
|
444
|
+
const active = activeScopes.get(scope);
|
|
445
|
+
if (active) {
|
|
446
|
+
active.rescheduleRequested = true;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const state = { rescheduleRequested: false };
|
|
450
|
+
activeScopes.set(scope, state);
|
|
451
|
+
setImmediate(() => {
|
|
452
|
+
const runtime = new DurableJobRuntime();
|
|
453
|
+
const executor = new PackageHandoffJobExecutor(runtime);
|
|
454
|
+
void (async () => {
|
|
455
|
+
try {
|
|
456
|
+
do {
|
|
457
|
+
state.rescheduleRequested = false;
|
|
458
|
+
await executor.runUntilIdle(projectId, workspaceId);
|
|
459
|
+
} while (rescheduleRequested(state));
|
|
460
|
+
}
|
|
461
|
+
finally {
|
|
462
|
+
runtime.close();
|
|
463
|
+
activeScopes.delete(scope);
|
|
464
|
+
}
|
|
465
|
+
})();
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function rescheduleRequested(state) {
|
|
469
|
+
return state.rescheduleRequested;
|
|
470
|
+
}
|
|
471
|
+
/** Re-enqueues accepted/checkpointed package-handoff work discovered after a process restart. */
|
|
472
|
+
export function recoverPackageHandoffJobsAtStartup() {
|
|
473
|
+
const runtime = new DurableJobRuntime();
|
|
474
|
+
try {
|
|
475
|
+
const scopes = runtime.listRunnableScopes();
|
|
476
|
+
for (const scope of scopes) {
|
|
477
|
+
const hasHandoffWork = runtime
|
|
478
|
+
.list(scope.projectId, scope.workspaceId)
|
|
479
|
+
.some((job) => (job.state === 'accepted' || job.state === 'checkpointed') &&
|
|
480
|
+
isPackageHandoffJob(job.payload));
|
|
481
|
+
if (hasHandoffWork) {
|
|
482
|
+
schedulePackageHandoffJobExecution(scope.projectId, scope.workspaceId);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
runtime.close();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
//# sourceMappingURL=package-handoff-job-executor.js.map
|
|
@@ -13,7 +13,7 @@ import { resolveStorageLayout } from '../../storage/storage-layout.js';
|
|
|
13
13
|
import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
|
|
14
14
|
import { createValidationReceiptAuthority, ensureValidationIssuerKey, issueValidationReceipt, LOCAL_VALIDATION_RECEIPT_ISSUER, preflightValidationReceiptSigner, } from '../validation/validation-receipt.js';
|
|
15
15
|
import { appendArtifact, readArtifact } from '../handoff-artifacts/io.js';
|
|
16
|
-
import {
|
|
16
|
+
import { beginValidationFreshnessBarrier, beginValidationFreshnessPublicationWindow, bindValidationFreshnessLease, closeValidationFreshnessLease, evaluateValidationFreshnessBarrier, rebaselineValidationFreshnessLeaseAfterRecompute, } from '../validation/validation-freshness.js';
|
|
17
17
|
import { recoverValidateJobs } from './validate-job-recovery.js';
|
|
18
18
|
import { getRuntimePolicy } from '../runtime-policy.js';
|
|
19
19
|
import { isValidateJob, parseValidatePayload, rehydrateValidationPayload, } from './validate-job-payload.js';
|
|
@@ -148,6 +148,16 @@ function toolResultError(result) {
|
|
|
148
148
|
function executionAborted(signal) {
|
|
149
149
|
return signal.aborted;
|
|
150
150
|
}
|
|
151
|
+
function actionableFreshnessMessage(prefix, reason, changedInput) {
|
|
152
|
+
const changedSuffix = changedInput ? ` (changed: ${changedInput})` : '';
|
|
153
|
+
return `${prefix}: ${reason}${changedSuffix} Re-run validate to issue a fresh receipt, then retry the original call.`;
|
|
154
|
+
}
|
|
155
|
+
function freshnessBarrierErrorMessage(phase, result) {
|
|
156
|
+
return actionableFreshnessMessage(`[Planu] Validation inputs changed ${phase} receipt publication`, result.reason, result.changedInput);
|
|
157
|
+
}
|
|
158
|
+
function rebaselineRejectionErrorMessage(rejection) {
|
|
159
|
+
return actionableFreshnessMessage('[Planu] Validation inputs changed before receipt publication', rejection.reason, rejection.changedInput);
|
|
160
|
+
}
|
|
151
161
|
function hasReceiptInputs(payload) {
|
|
152
162
|
return Boolean(payload.bindings && payload.artifactDigests && payload.freshnessLease);
|
|
153
163
|
}
|
|
@@ -688,8 +698,9 @@ export class ValidateJobExecutor {
|
|
|
688
698
|
if (executionAborted(signal)) {
|
|
689
699
|
signal.throwIfAborted();
|
|
690
700
|
}
|
|
691
|
-
|
|
692
|
-
|
|
701
|
+
const barrierBeforePublication = await evaluateValidationFreshnessBarrier(publicationLease, receiptBindings);
|
|
702
|
+
if (!barrierBeforePublication.fresh) {
|
|
703
|
+
throw new Error(freshnessBarrierErrorMessage('before', barrierBeforePublication));
|
|
693
704
|
}
|
|
694
705
|
const database = new RuntimeDatabase({ path: resolveStorageLayout().runtimeDatabase });
|
|
695
706
|
try {
|
|
@@ -723,9 +734,10 @@ export class ValidateJobExecutor {
|
|
|
723
734
|
if (executionAborted(signal)) {
|
|
724
735
|
signal.throwIfAborted();
|
|
725
736
|
}
|
|
726
|
-
|
|
737
|
+
const barrierDuringPublication = await evaluateValidationFreshnessBarrier(publicationLease, receiptBindings);
|
|
738
|
+
if (!barrierDuringPublication.fresh) {
|
|
727
739
|
await store.invalidateCurrentBindings(identity.projectId, payload.bindings.specId);
|
|
728
|
-
throw new Error('
|
|
740
|
+
throw new Error(freshnessBarrierErrorMessage('during', barrierDuringPublication));
|
|
729
741
|
}
|
|
730
742
|
const receiptKey = `validation-receipt:${receipt.receiptId.slice('sha256:'.length)}`;
|
|
731
743
|
let enrichedReportSha;
|
|
@@ -775,10 +787,10 @@ export class ValidateJobExecutor {
|
|
|
775
787
|
return toValidationReceiptBindings(observed.bindings, observed.artifactDigests);
|
|
776
788
|
},
|
|
777
789
|
});
|
|
778
|
-
if (
|
|
779
|
-
throw new Error(
|
|
790
|
+
if ('rejection' in rebaselined) {
|
|
791
|
+
throw new Error(rebaselineRejectionErrorMessage(rebaselined.rejection));
|
|
780
792
|
}
|
|
781
|
-
lease = rebaselined;
|
|
793
|
+
lease = rebaselined.lease;
|
|
782
794
|
return lease;
|
|
783
795
|
}
|
|
784
796
|
async persistReceiptArtifacts(identity, payload, receiptId, receiptKey) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// engine/git/exec-git.ts — SPEC-1560: bounded synchronous git capture helper
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
export const GIT_EXEC_TIMEOUT_MS = 5_000;
|
|
4
|
+
export const GIT_EXEC_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
5
|
+
export function execGitCapture(command, cwd) {
|
|
6
|
+
return execSync(command, {
|
|
7
|
+
cwd,
|
|
8
|
+
timeout: GIT_EXEC_TIMEOUT_MS,
|
|
9
|
+
maxBuffer: GIT_EXEC_MAX_BUFFER_BYTES,
|
|
10
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
11
|
+
encoding: 'utf-8',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=exec-git.js.map
|