@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
|
@@ -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
|
|
@@ -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
|