@planu/cli 5.3.63 → 5.3.64
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 +29 -0
- package/dist/.planu-build.json +1 -1
- package/dist/cli/commands/create.js +3 -16
- package/dist/cli/commands/dashboard.js +2 -16
- package/dist/cli/commands/init.js +2 -16
- package/dist/cli/commands/spec.js +5 -36
- package/dist/cli/commands/status.js +5 -28
- package/dist/cli/formatter.d.ts +7 -1
- package/dist/cli/formatter.js +49 -6
- package/dist/engine/cascade-hooks/hooks/implementing-kickoff.hook.js +38 -6
- package/dist/engine/dor-dod/index.d.ts +1 -1
- package/dist/engine/dor-dod/index.js +1 -1
- package/dist/engine/dor-dod/plan.d.ts +1 -0
- package/dist/engine/dor-dod/plan.js +2 -0
- package/dist/engine/evidence-gates/artifact-reader.js +1 -0
- package/dist/engine/evidence-gates/evidence-skeletons.js +1 -0
- package/dist/engine/execution-plan/plan-utils.d.ts +2 -6
- package/dist/engine/execution-plan/plan-utils.js +71 -37
- package/dist/engine/handoff-artifacts/implementation-review-reader.d.ts +4 -0
- package/dist/engine/handoff-artifacts/implementation-review-reader.js +72 -0
- package/dist/engine/validator/dor-dod.d.ts +2 -2
- package/dist/engine/validator/dor-dod.js +5 -4
- package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.d.ts +2 -4
- package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.js +24 -24
- package/dist/tools/challenge-spec/resilience-challenge-scenarios.js +1 -1
- package/dist/tools/challenge-spec/scenarios-failure.js +1 -2
- package/dist/tools/challenge-spec/scenarios-utils.d.ts +3 -0
- package/dist/tools/challenge-spec/scenarios-utils.js +16 -0
- package/dist/tools/challenge-spec-helpers.d.ts +1 -1
- package/dist/tools/challenge-spec-helpers.js +5 -2
- package/dist/tools/challenge-spec.js +5 -2
- package/dist/tools/create-spec.js +18 -0
- package/dist/tools/generate-execution-plan.js +160 -54
- package/dist/tools/update-status/dod-gates.js +46 -64
- package/dist/tools/update-status/side-effects.d.ts +4 -0
- package/dist/tools/update-status/side-effects.js +45 -4
- package/dist/tools/validate.js +8 -4
- package/dist/transports/transport-factory.js +13 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/evidence-gates.d.ts +2 -0
- package/dist/types/execution.d.ts +5 -0
- package/dist/types/handoff-artifacts.d.ts +19 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
- package/scripts/lib/portable-paths.mjs +1 -0
- package/dist/engine/execution-plan/phases-b.d.ts +0 -4
- package/dist/engine/execution-plan/phases-b.js +0 -88
- package/dist/engine/execution-plan/phases.d.ts +0 -7
- package/dist/engine/execution-plan/phases.js +0 -216
|
@@ -17,9 +17,10 @@ import { formatKeyValue } from '../output-formatter.js';
|
|
|
17
17
|
import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
|
|
18
18
|
import { verifyCurrentDoneReceipt } from './done-receipt-verifier.js';
|
|
19
19
|
import { projectDataDir } from '../../storage/base-store.js';
|
|
20
|
-
import {
|
|
20
|
+
import { transitionLogPath } from '../../storage/transition-log.js';
|
|
21
21
|
import { ImplementationReviewV1Schema, ReviewFeedbackV1Schema, ValidationReportV1Schema, } from '../../engine/handoff-artifacts/schemas.js';
|
|
22
22
|
import { renderSchemaSkeleton } from '../../engine/handoff-artifacts/schema-skeleton.js';
|
|
23
|
+
import { isLifecycleStubBody, readVerifiedImplementationReview, } from '../../engine/handoff-artifacts/implementation-review-reader.js';
|
|
23
24
|
export { completedJobPublishesReceipt } from './done-receipt-verifier.js';
|
|
24
25
|
const HANDOFF_ARTIFACT_TEMPLATE_TIMESTAMP = '2026-01-01T00:00:00.000Z';
|
|
25
26
|
const HANDOFF_ARTIFACT_TEMPLATE_SCHEMA_VERSION = '1.0.0';
|
|
@@ -186,7 +187,8 @@ export async function runValidateGate(spec, projectPath, forceStatus, forceStatu
|
|
|
186
187
|
* Returns an error ToolResult if blocked (unless force=true), null if passed.
|
|
187
188
|
*/
|
|
188
189
|
export async function checkDodGate(spec, specId, projectId, projectPath, _force) {
|
|
189
|
-
const
|
|
190
|
+
const verifiedReview = await readVerifiedImplementationReview(projectId, specId);
|
|
191
|
+
const dod = await generateDoD(spec, undefined, undefined, projectPath, verifiedReview);
|
|
190
192
|
const requiredItems = dod.items.filter((i) => i.required);
|
|
191
193
|
const blockingItems = requiredItems
|
|
192
194
|
.filter((i) => i.status === 'failed')
|
|
@@ -591,10 +593,6 @@ export async function readApprovedValidationReportGate(specId, projectId, force)
|
|
|
591
593
|
};
|
|
592
594
|
}
|
|
593
595
|
}
|
|
594
|
-
function isLifecycleStubBody(specId, body) {
|
|
595
|
-
return (body === `Spec ${specId} reviewed by planu-spec-reviewer and approved for approval gates.` ||
|
|
596
|
-
body === `Spec ${specId} reviewed by planu-spec-reviewer with requested changes.`);
|
|
597
|
-
}
|
|
598
596
|
/** SPEC-1051: Approval requires a dedicated spec reviewer artifact. */
|
|
599
597
|
export async function checkSpecReviewGate(specId, projectId, _forceApprove) {
|
|
600
598
|
try {
|
|
@@ -665,28 +663,36 @@ export async function checkSpecReviewGate(specId, projectId, _forceApprove) {
|
|
|
665
663
|
}
|
|
666
664
|
}
|
|
667
665
|
export async function checkImplementationReviewGate(specId, projectId, _force) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
666
|
+
const verified = await readVerifiedImplementationReview(projectId, specId);
|
|
667
|
+
if (verified.ok) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
switch (verified.reason) {
|
|
671
|
+
case 'missing':
|
|
672
|
+
case 'schema_invalid':
|
|
673
673
|
return implementationReviewGateError({
|
|
674
674
|
specId,
|
|
675
|
-
error:
|
|
675
|
+
error: verified.reason === 'missing'
|
|
676
676
|
? 'implementation_review_missing'
|
|
677
677
|
: 'implementation_review_invalid',
|
|
678
|
-
message:
|
|
678
|
+
message: verified.reason === 'missing'
|
|
679
679
|
? 'No implementation review exists for this spec. An independent planu-implementation-reviewer must review the implementation and write its own evidence before done.'
|
|
680
680
|
: 'Implementation review evidence is malformed or uses an obsolete schema.',
|
|
681
|
-
blockers: schemaBlockers(
|
|
682
|
-
fixHint:
|
|
681
|
+
blockers: schemaBlockers(verified.errors ?? []),
|
|
682
|
+
fixHint: verified.reason === 'missing'
|
|
683
683
|
? 'Have planu-implementation-reviewer write implementation_review.json (ImplementationReviewV1) into the handoff store, then retry update_status(done).'
|
|
684
684
|
: 'Have planu-implementation-reviewer rewrite implementation_review.json as valid ImplementationReviewV1 evidence, then retry done.',
|
|
685
685
|
template: implementationReviewTemplate(),
|
|
686
686
|
});
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
687
|
+
case 'wrong_spec':
|
|
688
|
+
return implementationReviewGateError({
|
|
689
|
+
specId,
|
|
690
|
+
error: 'implementation_review_wrong_spec',
|
|
691
|
+
message: 'Implementation review evidence is bound to a different spec.',
|
|
692
|
+
blockers: [`Review specId was ${verified.review?.specId}. Expected ${specId}.`],
|
|
693
|
+
fixHint: 'Have planu-implementation-reviewer write review evidence bound to this spec, then retry done.',
|
|
694
|
+
});
|
|
695
|
+
case 'stub_body':
|
|
690
696
|
return implementationReviewGateError({
|
|
691
697
|
specId,
|
|
692
698
|
error: 'implementation_review_stub',
|
|
@@ -694,24 +700,22 @@ export async function checkImplementationReviewGate(specId, projectId, _force) {
|
|
|
694
700
|
blockers: [],
|
|
695
701
|
fixHint: 'Replace the stub with genuine planu-implementation-reviewer evidence in the handoff store, then retry done.',
|
|
696
702
|
});
|
|
697
|
-
|
|
698
|
-
if (review.verdict !== 'approved' || review.reviewer.verdict !== 'approved') {
|
|
703
|
+
case 'not_approved':
|
|
699
704
|
return implementationReviewGateError({
|
|
700
705
|
specId,
|
|
701
706
|
error: 'implementation_review_changes_requested',
|
|
702
707
|
message: 'Implementation reviewer requested changes. Done is blocked until the review passes.',
|
|
703
|
-
blockers: review
|
|
708
|
+
blockers: verified.review?.blockers ?? [],
|
|
704
709
|
fixHint: 'Resolve the implementation review blockers and have planu-implementation-reviewer write updated evidence, then retry done.',
|
|
705
710
|
});
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
if (implementationReviewerBlockers.length > 0) {
|
|
711
|
+
case 'wrong_agent_or_kind': {
|
|
712
|
+
const implementationReviewerBlockers = [];
|
|
713
|
+
if (verified.review?.reviewer.kind !== 'implementation-review-agent') {
|
|
714
|
+
implementationReviewerBlockers.push(`reviewer.kind was ${verified.review?.reviewer.kind}. Expected implementation-review-agent.`);
|
|
715
|
+
}
|
|
716
|
+
if (verified.review?.reviewer.agent !== 'planu-implementation-reviewer') {
|
|
717
|
+
implementationReviewerBlockers.push(`reviewer.agent was ${verified.review?.reviewer.agent}. Expected planu-implementation-reviewer.`);
|
|
718
|
+
}
|
|
715
719
|
return implementationReviewGateError({
|
|
716
720
|
specId,
|
|
717
721
|
error: 'implementation_review_wrong_reviewer',
|
|
@@ -720,54 +724,32 @@ export async function checkImplementationReviewGate(specId, projectId, _force) {
|
|
|
720
724
|
fixHint: 'Have planu-implementation-reviewer write the review artifact with reviewer.kind implementation-review-agent and reviewer.agent planu-implementation-reviewer, then retry done.',
|
|
721
725
|
});
|
|
722
726
|
}
|
|
723
|
-
|
|
724
|
-
try {
|
|
725
|
-
implementerActor = await findImplementingActor(specId, projectId);
|
|
726
|
-
}
|
|
727
|
-
catch (err) {
|
|
728
|
-
/* reliability-optional: TRANSITION_LOG_GATE_READ — typed gate error blocks done */
|
|
729
|
-
reportClassifiedDegradation('TRANSITION_LOG_GATE_READ', err);
|
|
727
|
+
case 'actor_unreadable':
|
|
730
728
|
return implementationReviewGateError({
|
|
731
729
|
specId,
|
|
732
730
|
error: 'implementing_actor_unreadable',
|
|
733
|
-
message: `Could not read the transition log to find the implementing actor: ${
|
|
731
|
+
message: `Could not read the transition log to find the implementing actor: ${verified.readError ?? ''}`,
|
|
734
732
|
blockers: [],
|
|
735
733
|
fixHint: 'Fix the transition log so it can be read, then retry update_status(done).',
|
|
736
734
|
artifactPath: transitionLogPath(projectId),
|
|
737
735
|
});
|
|
738
|
-
|
|
739
|
-
if (implementerActor !== undefined && implementerActor === review.reviewer.agent) {
|
|
736
|
+
case 'self_review':
|
|
740
737
|
return implementationReviewGateError({
|
|
741
738
|
specId,
|
|
742
739
|
error: 'implementation_review_self_review',
|
|
743
740
|
message: 'The implementation reviewer must be a different identity from the implementer. Self-review is blocked.',
|
|
744
|
-
blockers: [`Implementer and reviewer share the identity "${
|
|
741
|
+
blockers: [`Implementer and reviewer share the identity "${verified.implementingActor}".`],
|
|
745
742
|
fixHint: 'Have a different, independent agent write the implementation review evidence, then retry done.',
|
|
746
743
|
});
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
error: 'implementation_review_unreadable',
|
|
756
|
-
message: `Could not read implementation review evidence: ${err instanceof Error ? err.message : String(err)}`,
|
|
757
|
-
blockers: [],
|
|
758
|
-
fixHint: 'Fix the artifact store issue so implementation_review.json is readable, then retry update_status(done).',
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
async function findImplementingActor(specId, projectId) {
|
|
763
|
-
const entries = await readTransitionLog(projectId, specId);
|
|
764
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
765
|
-
const entry = entries[i];
|
|
766
|
-
if (entry?.to === 'implementing' && entry.actor) {
|
|
767
|
-
return entry.actor;
|
|
768
|
-
}
|
|
744
|
+
case 'unreadable':
|
|
745
|
+
return implementationReviewGateError({
|
|
746
|
+
specId,
|
|
747
|
+
error: 'implementation_review_unreadable',
|
|
748
|
+
message: `Could not read implementation review evidence: ${verified.readError ?? ''}`,
|
|
749
|
+
blockers: [],
|
|
750
|
+
fixHint: 'Fix the artifact store issue so implementation_review.json is readable, then retry update_status(done).',
|
|
751
|
+
});
|
|
769
752
|
}
|
|
770
|
-
return undefined;
|
|
771
753
|
}
|
|
772
754
|
function implementationReviewGateError(args) {
|
|
773
755
|
const artifactPath = args.artifactPath ??
|
|
@@ -2,6 +2,10 @@ import type { SpecStatus } from '../../types/index.js';
|
|
|
2
2
|
import { type knowledgeStore } from '../../storage/index.js';
|
|
3
3
|
import type { Spec } from '../../types/spec/core.js';
|
|
4
4
|
import type { RunCascadeResult } from '../../types/cascade-hooks.js';
|
|
5
|
+
export declare function drainPendingPostCommitWork(boundMs: number): Promise<{
|
|
6
|
+
pendingCount: number;
|
|
7
|
+
drained: boolean;
|
|
8
|
+
}>;
|
|
5
9
|
export interface PostCommitTask {
|
|
6
10
|
name: string;
|
|
7
11
|
run: (signal?: AbortSignal) => Promise<unknown>;
|
|
@@ -3,7 +3,48 @@ import { runCascade } from '../../engine/cascade-hooks/runner.js';
|
|
|
3
3
|
import { appendAutopilotLogEntry } from '../../storage/autopilot-log-store.js';
|
|
4
4
|
import { redactFailureMessage, reportClassifiedDegradation, } from '../../errors/classified-degradation.js';
|
|
5
5
|
import { getRuntimePolicy } from '../../engine/runtime-policy.js';
|
|
6
|
+
import { createExecutionScope } from '../../engine/execution/context.js';
|
|
6
7
|
const queuedTransitions = new Map();
|
|
8
|
+
const CASCADE_ISOLATED_SCOPE_MS = 30_000;
|
|
9
|
+
const pendingPostCommitWork = new Set();
|
|
10
|
+
function resolveTaskLeaseMs(task) {
|
|
11
|
+
const policy = getRuntimePolicy().postCommit;
|
|
12
|
+
const timeoutMs = task.timeoutMs ?? policy.defaultTaskTimeoutMs;
|
|
13
|
+
return Math.max(policy.minimumLeaseMs, timeoutMs * policy.leaseMultiplier);
|
|
14
|
+
}
|
|
15
|
+
function trackIsolatedWork(deadlineAt, fn) {
|
|
16
|
+
const scope = createExecutionScope({ deadlineAt });
|
|
17
|
+
const work = scope.run(fn).finally(() => {
|
|
18
|
+
scope.dispose();
|
|
19
|
+
});
|
|
20
|
+
pendingPostCommitWork.add(work);
|
|
21
|
+
const untrack = () => {
|
|
22
|
+
pendingPostCommitWork.delete(work);
|
|
23
|
+
};
|
|
24
|
+
work.then(untrack, untrack);
|
|
25
|
+
return work;
|
|
26
|
+
}
|
|
27
|
+
export async function drainPendingPostCommitWork(boundMs) {
|
|
28
|
+
const pending = [...pendingPostCommitWork];
|
|
29
|
+
if (pending.length === 0) {
|
|
30
|
+
return { pendingCount: 0, drained: true };
|
|
31
|
+
}
|
|
32
|
+
let timer;
|
|
33
|
+
const timeout = new Promise((resolve) => {
|
|
34
|
+
timer = setTimeout(() => {
|
|
35
|
+
resolve('timeout');
|
|
36
|
+
}, boundMs);
|
|
37
|
+
timer.unref();
|
|
38
|
+
});
|
|
39
|
+
const outcome = await Promise.race([
|
|
40
|
+
Promise.allSettled(pending).then(() => 'settled'),
|
|
41
|
+
timeout,
|
|
42
|
+
]);
|
|
43
|
+
if (timer !== undefined) {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
}
|
|
46
|
+
return { pendingCount: pending.length, drained: outcome === 'settled' };
|
|
47
|
+
}
|
|
7
48
|
function rememberTransition(transitionKey) {
|
|
8
49
|
if (queuedTransitions.has(transitionKey)) {
|
|
9
50
|
return false;
|
|
@@ -21,7 +62,7 @@ async function runBoundedPostCommitTask(args) {
|
|
|
21
62
|
const { projectId, specId, transitionId, task } = args;
|
|
22
63
|
const policy = getRuntimePolicy().postCommit;
|
|
23
64
|
const timeoutMs = task.timeoutMs ?? policy.defaultTaskTimeoutMs;
|
|
24
|
-
const leaseMs =
|
|
65
|
+
const leaseMs = resolveTaskLeaseMs(task);
|
|
25
66
|
const claim = await specStore.claimPostCommitTask(projectId, specId, transitionId, task.name, {
|
|
26
67
|
leaseMs,
|
|
27
68
|
});
|
|
@@ -120,7 +161,7 @@ export function queuePostCommitTasks(args) {
|
|
|
120
161
|
if (!rememberTransition(transitionKey)) {
|
|
121
162
|
return [];
|
|
122
163
|
}
|
|
123
|
-
void Promise.allSettled(args.tasks.map((task) => runBoundedPostCommitTask({ ...args, task }))).finally(() => {
|
|
164
|
+
void Promise.allSettled(args.tasks.map((task) => trackIsolatedWork(Date.now() + resolveTaskLeaseMs(task), () => runBoundedPostCommitTask({ ...args, task })))).finally(() => {
|
|
124
165
|
queuedTransitions.delete(transitionKey);
|
|
125
166
|
});
|
|
126
167
|
return args.tasks.map((task) => task.name);
|
|
@@ -205,13 +246,13 @@ export function fireAndForgetSideEffects(params) {
|
|
|
205
246
|
allSpecs,
|
|
206
247
|
};
|
|
207
248
|
// Launch cascade — fire-and-forget, never blocks the response
|
|
208
|
-
void (async () => {
|
|
249
|
+
void trackIsolatedWork(Date.now() + CASCADE_ISOLATED_SCOPE_MS, async () => {
|
|
209
250
|
const [disabledByConfig, disabledByEnv] = await Promise.all([
|
|
210
251
|
readDisabledByConfig(projectPath),
|
|
211
252
|
Promise.resolve(readDisabledByEnv()),
|
|
212
253
|
]);
|
|
213
254
|
await runCascade(ctx, { disabledByConfig, disabledByEnv });
|
|
214
|
-
})
|
|
255
|
+
}).catch((error) => {
|
|
215
256
|
/* reliability-optional: ASYNC_CASCADE_FAILURE — post-commit task ledger captures retry state */
|
|
216
257
|
reportClassifiedDegradation('ASYNC_CASCADE_FAILURE', error);
|
|
217
258
|
});
|
package/dist/tools/validate.js
CHANGED
|
@@ -59,6 +59,7 @@ import { resolveProjectId, missingProjectIdError } from './resolve-project-id.js
|
|
|
59
59
|
import { specStore, knowledgeStore } from '../storage/index.js';
|
|
60
60
|
import { validateSpec, generateDoR, generateDoD } from '../engine/validator.js';
|
|
61
61
|
import { isBlockingQualitySeverity } from '../engine/validator/dor-dod.js';
|
|
62
|
+
import { readVerifiedImplementationReview } from '../engine/handoff-artifacts/implementation-review-reader.js';
|
|
62
63
|
import { calcQualityScore } from '../engine/auditor-scoring.js';
|
|
63
64
|
import { dispatchFeedbackEvent } from './learn.js';
|
|
64
65
|
import { t, ti } from '../i18n/index.js';
|
|
@@ -350,10 +351,13 @@ export async function executeValidate(args, server, onProgress) {
|
|
|
350
351
|
}
|
|
351
352
|
const result = await runStage('spec-scan', 'validate spec implementation', () => validateSpec(executionSpec, projectPath));
|
|
352
353
|
const graphCoverage = await runStage('graph', 'validate graph coverage', () => buildGraphCoverageReport({ projectId, projectPath, specId }));
|
|
353
|
-
const { dor, dod } = await runStage('definitions', 'validate ready and done definitions', async () =>
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
354
|
+
const { dor, dod } = await runStage('definitions', 'validate ready and done definitions', async () => {
|
|
355
|
+
const verifiedReview = await readVerifiedImplementationReview(projectId, specId);
|
|
356
|
+
return {
|
|
357
|
+
dor: generateDoR(executionSpec),
|
|
358
|
+
dod: await generateDoD(executionSpec, result, undefined, projectPath, verifiedReview),
|
|
359
|
+
};
|
|
360
|
+
});
|
|
357
361
|
const implementationQualityScore = calcQualityScore(result.qualityIssues);
|
|
358
362
|
const auditedFiles = [...new Set(result.qualityIssues.map((i) => i.file))];
|
|
359
363
|
const { conventionViolations, regressionDetected } = await runStage('conventions', 'validate project conventions', () => scanProjectConventions(projectId, projectPath));
|
|
@@ -3,10 +3,12 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { createHttpTransport } from './http-transport.js';
|
|
5
5
|
import { compactToolsListMessage, isToolsListResponse, } from '../engine/compact/tool-list-compactor.js';
|
|
6
|
+
import { reportClassifiedDegradation } from '../errors/classified-degradation.js';
|
|
6
7
|
const VALID_TRANSPORTS = ['stdio', 'http'];
|
|
7
8
|
const DEFAULT_PORT = 3100;
|
|
8
9
|
const DEFAULT_HOST = '127.0.0.1';
|
|
9
10
|
const SHUTDOWN_GRACE_MS = 100;
|
|
11
|
+
const POST_COMMIT_DRAIN_MS = 1_200;
|
|
10
12
|
function descendantPids(parentPid) {
|
|
11
13
|
if (process.platform === 'win32') {
|
|
12
14
|
return [];
|
|
@@ -114,6 +116,13 @@ function installToolsListCompaction(transport) {
|
|
|
114
116
|
const wrappedTransport = transport;
|
|
115
117
|
wrappedTransport.send = compactingSend;
|
|
116
118
|
}
|
|
119
|
+
async function drainPostCommitWorkOnShutdown() {
|
|
120
|
+
const { drainPendingPostCommitWork } = await import('../tools/update-status/side-effects.js');
|
|
121
|
+
const drainResult = await drainPendingPostCommitWork(POST_COMMIT_DRAIN_MS);
|
|
122
|
+
if (!drainResult.drained) {
|
|
123
|
+
reportClassifiedDegradation('POST_COMMIT_DRAIN_TIMEOUT', new Error(`Shutdown drain exceeded ${String(POST_COMMIT_DRAIN_MS)}ms with ${String(drainResult.pendingCount)} pending`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
117
126
|
function installStdioShutdownHandlers(server) {
|
|
118
127
|
let shuttingDown = false;
|
|
119
128
|
const handlers = new Map();
|
|
@@ -153,6 +162,10 @@ function installStdioShutdownHandlers(server) {
|
|
|
153
162
|
})
|
|
154
163
|
.catch(() => {
|
|
155
164
|
/* shutdown remains best-effort */
|
|
165
|
+
})
|
|
166
|
+
.then(() => drainPostCommitWorkOnShutdown())
|
|
167
|
+
.catch((err) => {
|
|
168
|
+
reportClassifiedDegradation('POST_COMMIT_DRAIN_UNAVAILABLE', err);
|
|
156
169
|
})
|
|
157
170
|
.then(() => Promise.all([
|
|
158
171
|
server.close(),
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ export interface CliCommand {
|
|
|
17
17
|
/** Execute the command with parsed args */
|
|
18
18
|
run(args: string[], flags?: GlobalFlags): Promise<void>;
|
|
19
19
|
}
|
|
20
|
+
export interface CliJsonFallbackEnvelope {
|
|
21
|
+
content: unknown[];
|
|
22
|
+
}
|
|
20
23
|
export interface StatusCommandValues {
|
|
21
24
|
set?: string;
|
|
22
25
|
'project-id'?: string;
|
|
@@ -20,9 +20,11 @@ export interface DiscoveryEvidence {
|
|
|
20
20
|
meaning: string;
|
|
21
21
|
}[];
|
|
22
22
|
}
|
|
23
|
+
export type TaskPlanGeneratedBy = 'planu-evidence-skeleton';
|
|
23
24
|
export interface TaskPlanEvidence {
|
|
24
25
|
version: 1;
|
|
25
26
|
provenance?: ValidationArtifactProvenance;
|
|
27
|
+
generatedBy?: TaskPlanGeneratedBy;
|
|
26
28
|
tasks: {
|
|
27
29
|
id: string;
|
|
28
30
|
title: string;
|
|
@@ -26,6 +26,11 @@ export interface ExecutionStep {
|
|
|
26
26
|
parallelizable?: boolean;
|
|
27
27
|
cycle?: PlanCycle;
|
|
28
28
|
}
|
|
29
|
+
export type ExecutionPlanGraphViolationCode = 'SELF_DEPENDENT_STEP' | 'INVALID_STEP_DEPENDENCY' | 'CYCLIC_STEP_DEPENDENCIES';
|
|
30
|
+
export interface ExecutionPlanGraphViolation {
|
|
31
|
+
code: ExecutionPlanGraphViolationCode;
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
29
34
|
export interface PhaseTracker {
|
|
30
35
|
currentPhase: SddPhase;
|
|
31
36
|
specId: string | null;
|
|
@@ -146,4 +146,23 @@ export interface ArtifactPayloadMap {
|
|
|
146
146
|
}
|
|
147
147
|
export type ArtifactPayload<K extends ArtifactKind> = ArtifactPayloadMap[K];
|
|
148
148
|
import type { MinimalImplementationReport } from './minimal-implementation-gate.js';
|
|
149
|
+
export type ImplementationReviewRejectionReason = 'missing' | 'unreadable' | 'schema_invalid' | 'stub_body' | 'wrong_agent_or_kind' | 'not_approved' | 'self_review' | 'actor_unreadable' | 'wrong_spec';
|
|
150
|
+
export interface VerifiedImplementationReviewOk {
|
|
151
|
+
ok: true;
|
|
152
|
+
review: ImplementationReviewV1;
|
|
153
|
+
implementingActor: string | null;
|
|
154
|
+
}
|
|
155
|
+
export interface VerifiedImplementationReviewErr {
|
|
156
|
+
ok: false;
|
|
157
|
+
reason: ImplementationReviewRejectionReason;
|
|
158
|
+
review?: ImplementationReviewV1;
|
|
159
|
+
implementingActor?: string;
|
|
160
|
+
errors?: {
|
|
161
|
+
code: string;
|
|
162
|
+
path: string;
|
|
163
|
+
message: string;
|
|
164
|
+
}[];
|
|
165
|
+
readError?: string;
|
|
166
|
+
}
|
|
167
|
+
export type VerifiedImplementationReviewResult = VerifiedImplementationReviewOk | VerifiedImplementationReviewErr;
|
|
149
168
|
//# sourceMappingURL=handoff-artifacts.d.ts.map
|
package/dist/types/index.d.ts
CHANGED
|
@@ -291,4 +291,5 @@ export * from './host-tool-filter.js';
|
|
|
291
291
|
export * from './reconcile.js';
|
|
292
292
|
export * from './release-pipeline.js';
|
|
293
293
|
export * from './network-policy.js';
|
|
294
|
+
export type { VerifiedImplementationReviewResult, VerifiedImplementationReviewOk, VerifiedImplementationReviewErr, ImplementationReviewRejectionReason, } from './handoff-artifacts.js';
|
|
294
295
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "5.3.
|
|
5
|
+
"version": "5.3.64",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|
|
@@ -47,6 +47,7 @@ export function portablePathPatterns({
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function redactPortablePaths(text, options = {}) {
|
|
50
|
+
if (typeof text !== 'string') return text;
|
|
50
51
|
return portablePathPatterns(options).reduce(
|
|
51
52
|
(redacted, { pattern, replacement }) => redacted.replace(pattern, replacement),
|
|
52
53
|
text,
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { Spec, ProjectKnowledge, ExecutionStep } from '../../types/index.js';
|
|
2
|
-
export declare function generateTestingPhase(spec: Spec, knowledge: ProjectKnowledge, startOrder: number): ExecutionStep[];
|
|
3
|
-
export declare function generateIntegrationPhase(_spec: Spec, knowledge: ProjectKnowledge, startOrder: number): ExecutionStep[];
|
|
4
|
-
//# sourceMappingURL=phases-b.d.ts.map
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// engine/execution-plan/phases-b.ts — Execution phase generators (part B)
|
|
2
|
-
// SRP: testing and integration phase generators.
|
|
3
|
-
// OCP: extend without modifying the dispatcher in generate-execution-plan.ts.
|
|
4
|
-
export function generateTestingPhase(spec, knowledge, startOrder) {
|
|
5
|
-
const steps = [];
|
|
6
|
-
let order = startOrder;
|
|
7
|
-
steps.push({
|
|
8
|
-
order: order++,
|
|
9
|
-
title: 'Write unit tests',
|
|
10
|
-
description: 'Test business logic, validators, and utilities in isolation',
|
|
11
|
-
files: [`tests/unit/${spec.slug}.test.ts`],
|
|
12
|
-
dependsOn: [startOrder - 1],
|
|
13
|
-
verification: `All unit tests pass: ${knowledge.testCommand ?? 'npm test'}`,
|
|
14
|
-
estimatedMinutes: 30,
|
|
15
|
-
canRollback: true,
|
|
16
|
-
});
|
|
17
|
-
if (spec.scope !== 'trivial') {
|
|
18
|
-
steps.push({
|
|
19
|
-
order: order++,
|
|
20
|
-
title: 'Write integration tests',
|
|
21
|
-
description: 'Test API endpoints and data layer together',
|
|
22
|
-
files: [`tests/integration/${spec.slug}.test.ts`],
|
|
23
|
-
dependsOn: [startOrder],
|
|
24
|
-
verification: 'Integration tests pass against test database',
|
|
25
|
-
estimatedMinutes: 30,
|
|
26
|
-
canRollback: true,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
if (spec.target === 'frontend' || spec.target === 'fullstack') {
|
|
30
|
-
steps.push({
|
|
31
|
-
order: order,
|
|
32
|
-
title: 'Write E2E tests',
|
|
33
|
-
description: 'Test critical user flows end-to-end (Playwright/Cypress)',
|
|
34
|
-
files: [`tests/e2e/${spec.slug}.spec.ts`],
|
|
35
|
-
dependsOn: [startOrder],
|
|
36
|
-
verification: 'E2E tests pass in headless browser',
|
|
37
|
-
estimatedMinutes: 30,
|
|
38
|
-
canRollback: true,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
return steps;
|
|
42
|
-
}
|
|
43
|
-
export function generateIntegrationPhase(_spec, knowledge, startOrder) {
|
|
44
|
-
const steps = [];
|
|
45
|
-
let order = startOrder;
|
|
46
|
-
steps.push({
|
|
47
|
-
order: order++,
|
|
48
|
-
title: 'Run linting and formatting',
|
|
49
|
-
description: 'Ensure code passes all lint rules and is properly formatted',
|
|
50
|
-
files: [],
|
|
51
|
-
dependsOn: [startOrder - 1],
|
|
52
|
-
verification: `Lint passes: ${knowledge.linting.detectedLinters[0]?.tool ?? 'eslint'} runs clean`,
|
|
53
|
-
estimatedMinutes: 10,
|
|
54
|
-
canRollback: true,
|
|
55
|
-
});
|
|
56
|
-
steps.push({
|
|
57
|
-
order: order++,
|
|
58
|
-
title: 'Update documentation',
|
|
59
|
-
description: 'Update README, API docs, and changelog if applicable',
|
|
60
|
-
files: [],
|
|
61
|
-
dependsOn: [startOrder],
|
|
62
|
-
verification: 'Documentation reflects the new changes',
|
|
63
|
-
estimatedMinutes: 15,
|
|
64
|
-
canRollback: true,
|
|
65
|
-
});
|
|
66
|
-
steps.push({
|
|
67
|
-
order: order++,
|
|
68
|
-
title: 'Full build and test suite',
|
|
69
|
-
description: 'Run complete build and all test suites to catch regressions',
|
|
70
|
-
files: [],
|
|
71
|
-
dependsOn: [order - 2, order - 1],
|
|
72
|
-
verification: `Full build succeeds: ${knowledge.buildCommand ?? 'npm run build'} && ${knowledge.testCommand ?? 'npm test'}`,
|
|
73
|
-
estimatedMinutes: 10,
|
|
74
|
-
canRollback: true,
|
|
75
|
-
});
|
|
76
|
-
steps.push({
|
|
77
|
-
order: order++,
|
|
78
|
-
title: 'Create pull request',
|
|
79
|
-
description: 'Push branch and create PR with spec reference and summary',
|
|
80
|
-
files: [],
|
|
81
|
-
dependsOn: [order - 2],
|
|
82
|
-
verification: 'PR created with passing CI checks',
|
|
83
|
-
estimatedMinutes: 10,
|
|
84
|
-
canRollback: true,
|
|
85
|
-
});
|
|
86
|
-
return steps;
|
|
87
|
-
}
|
|
88
|
-
//# sourceMappingURL=phases-b.js.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { Spec, ProjectKnowledge, ExecutionStep } from '../../types/index.js';
|
|
2
|
-
export { generateTestingPhase, generateIntegrationPhase } from './phases-b.js';
|
|
3
|
-
export declare function generateSetupPhase(spec: Spec, knowledge: ProjectKnowledge, startOrder: number): ExecutionStep[];
|
|
4
|
-
export declare function generateDataLayerPhase(spec: Spec, knowledge: ProjectKnowledge, content: string, startOrder: number): ExecutionStep[];
|
|
5
|
-
export declare function generateBusinessLogicPhase(spec: Spec, knowledge: ProjectKnowledge, content: string, startOrder: number): ExecutionStep[];
|
|
6
|
-
export declare function generateUIPhase(spec: Spec, knowledge: ProjectKnowledge, content: string, startOrder: number): ExecutionStep[];
|
|
7
|
-
//# sourceMappingURL=phases.d.ts.map
|