@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/.planu-build.json +1 -1
  3. package/dist/cli/commands/create.js +3 -16
  4. package/dist/cli/commands/dashboard.js +2 -16
  5. package/dist/cli/commands/init.js +2 -16
  6. package/dist/cli/commands/spec.js +5 -36
  7. package/dist/cli/commands/status.js +5 -28
  8. package/dist/cli/formatter.d.ts +7 -1
  9. package/dist/cli/formatter.js +49 -6
  10. package/dist/engine/cascade-hooks/hooks/implementing-kickoff.hook.js +38 -6
  11. package/dist/engine/dor-dod/index.d.ts +1 -1
  12. package/dist/engine/dor-dod/index.js +1 -1
  13. package/dist/engine/dor-dod/plan.d.ts +1 -0
  14. package/dist/engine/dor-dod/plan.js +2 -0
  15. package/dist/engine/evidence-gates/artifact-reader.js +1 -0
  16. package/dist/engine/evidence-gates/evidence-skeletons.js +1 -0
  17. package/dist/engine/execution-plan/plan-utils.d.ts +2 -6
  18. package/dist/engine/execution-plan/plan-utils.js +71 -37
  19. package/dist/engine/handoff-artifacts/implementation-review-reader.d.ts +4 -0
  20. package/dist/engine/handoff-artifacts/implementation-review-reader.js +72 -0
  21. package/dist/engine/validator/dor-dod.d.ts +2 -2
  22. package/dist/engine/validator/dor-dod.js +5 -4
  23. package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.d.ts +2 -4
  24. package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.js +24 -24
  25. package/dist/tools/challenge-spec/resilience-challenge-scenarios.js +1 -1
  26. package/dist/tools/challenge-spec/scenarios-failure.js +1 -2
  27. package/dist/tools/challenge-spec/scenarios-utils.d.ts +3 -0
  28. package/dist/tools/challenge-spec/scenarios-utils.js +16 -0
  29. package/dist/tools/challenge-spec-helpers.d.ts +1 -1
  30. package/dist/tools/challenge-spec-helpers.js +5 -2
  31. package/dist/tools/challenge-spec.js +5 -2
  32. package/dist/tools/create-spec.js +18 -0
  33. package/dist/tools/generate-execution-plan.js +160 -54
  34. package/dist/tools/update-status/dod-gates.js +46 -64
  35. package/dist/tools/update-status/side-effects.d.ts +4 -0
  36. package/dist/tools/update-status/side-effects.js +45 -4
  37. package/dist/tools/validate.js +8 -4
  38. package/dist/transports/transport-factory.js +13 -0
  39. package/dist/types/cli.d.ts +3 -0
  40. package/dist/types/evidence-gates.d.ts +2 -0
  41. package/dist/types/execution.d.ts +5 -0
  42. package/dist/types/handoff-artifacts.d.ts +19 -0
  43. package/dist/types/index.d.ts +1 -0
  44. package/package.json +1 -1
  45. package/planu-plugin.json +1 -1
  46. package/scripts/lib/portable-paths.mjs +1 -0
  47. package/dist/engine/execution-plan/phases-b.d.ts +0 -4
  48. package/dist/engine/execution-plan/phases-b.js +0 -88
  49. package/dist/engine/execution-plan/phases.d.ts +0 -7
  50. 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 { readTransitionLog, transitionLogPath } from '../../storage/transition-log.js';
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 dod = await generateDoD(spec, undefined, undefined, projectPath);
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
- try {
669
- const { readArtifact } = await import('../../engine/handoff-artifacts/io.js');
670
- const result = await readArtifact({ projectId, specId, kind: 'implementation_review' });
671
- if (!result.ok) {
672
- const firstErr = result.errors[0];
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: firstErr?.code === 'ARTIFACT_NOT_FOUND'
675
+ error: verified.reason === 'missing'
676
676
  ? 'implementation_review_missing'
677
677
  : 'implementation_review_invalid',
678
- message: firstErr?.code === 'ARTIFACT_NOT_FOUND'
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(result.errors),
682
- fixHint: firstErr?.code === 'ARTIFACT_NOT_FOUND'
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
- const review = result.payload;
689
- if (isLifecycleStubBody(specId, review.body)) {
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.blockers,
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
- const implementationReviewerBlockers = [];
708
- if (review.reviewer.kind !== 'implementation-review-agent') {
709
- implementationReviewerBlockers.push(`reviewer.kind was ${review.reviewer.kind}. Expected implementation-review-agent.`);
710
- }
711
- if (review.reviewer.agent !== 'planu-implementation-reviewer') {
712
- implementationReviewerBlockers.push(`reviewer.agent was ${review.reviewer.agent}. Expected planu-implementation-reviewer.`);
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
- let implementerActor;
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: ${err instanceof Error ? err.message : String(err)}`,
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 "${implementerActor}".`],
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
- return null;
749
- }
750
- catch (err) {
751
- /* reliability-optional: IMPLEMENTATION_REVIEW_GATE_READ typed gate error blocks done */
752
- reportClassifiedDegradation('IMPLEMENTATION_REVIEW_GATE_READ', err);
753
- return implementationReviewGateError({
754
- specId,
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 = Math.max(policy.minimumLeaseMs, timeoutMs * policy.leaseMultiplier);
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
- })().catch((error) => {
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
  });
@@ -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
- dor: generateDoR(executionSpec),
355
- dod: await generateDoD(executionSpec, result, undefined, projectPath),
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(),
@@ -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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "5.3.63",
3
+ "version": "5.3.64",
4
4
  "description": "Planu — MCP Server for Spec Driven Development. Cross-platform (Linux/macOS/Windows, x64/arm64).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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.63",
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