@planu/cli 4.11.1 → 4.11.2

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 (116) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/engine/cascade-hooks/registry.js +0 -2
  3. package/dist/engine/challenge-scenarios-loader.d.ts +3 -3
  4. package/dist/engine/compliance-injector.d.ts +3 -3
  5. package/dist/engine/dashboard/kanban-logic.d.ts +1 -1
  6. package/dist/engine/doc-generator/portal/risk-matrix-svg.d.ts +1 -1
  7. package/dist/engine/doc-generator/proposal/proposal-generator.d.ts +1 -1
  8. package/dist/engine/elicitation/answer-extractor.d.ts +1 -4
  9. package/dist/engine/elicitation/answer-extractor.js +197 -68
  10. package/dist/engine/elicitation/decision-gap-detector.js +59 -79
  11. package/dist/engine/elicitation/index.d.ts +0 -1
  12. package/dist/engine/elicitation/index.js +0 -1
  13. package/dist/engine/elicitation/question-grounding-gate.js +83 -34
  14. package/dist/engine/evidence-gates/lifecycle-gate.js +27 -14
  15. package/dist/engine/evidence-index/index-builder.js +7 -2
  16. package/dist/engine/frontmatter-sha/frozen-fields.d.ts +1 -1
  17. package/dist/engine/handoff-artifacts/schemas/red-tests-handoff.d.ts +2 -2
  18. package/dist/engine/handoff-artifacts/schemas.d.ts +22 -22
  19. package/dist/engine/human-summary.js +3 -3
  20. package/dist/engine/implementation-contract/common.d.ts +1 -1
  21. package/dist/engine/keyword-matcher/stopwords.d.ts +2 -2
  22. package/dist/engine/provider-adapters/adapters/claude.d.ts +1 -1
  23. package/dist/engine/provider-adapters/adapters/gemini.d.ts +1 -1
  24. package/dist/engine/provider-adapters/adapters/gpt4.d.ts +1 -1
  25. package/dist/engine/provider-adapters/adapters/markdown.d.ts +1 -1
  26. package/dist/engine/safety/atomic-write-file.d.ts +1 -1
  27. package/dist/engine/safety/cross-process-lock.d.ts +1 -1
  28. package/dist/engine/safety/path-sanitizer.d.ts +1 -1
  29. package/dist/engine/security-analyzer/profile-detector.js +48 -16
  30. package/dist/engine/spec-format/copula-blacklist.d.ts +2 -2
  31. package/dist/engine/spec-format/metadata-value-policy.d.ts +20 -20
  32. package/dist/engine/text-signal-boundaries.js +45 -12
  33. package/dist/engine/universal-rules/rules/planu-modes.js +0 -2
  34. package/dist/engine/well-known/manifest-schema.d.ts +33 -33
  35. package/dist/engine/workers/schema.d.ts +3 -3
  36. package/dist/hosts/claude-code/ux/mcp-resources.d.ts +4 -4
  37. package/dist/hosts/codex/rest-shim.d.ts +1 -1
  38. package/dist/native/lightweight-command-catalog.d.ts +30 -30
  39. package/dist/server/openapi-spec.d.ts +132 -132
  40. package/dist/tools/challenge-spec/event-challenge-scenarios.js +8 -27
  41. package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.js +16 -32
  42. package/dist/tools/challenge-spec/resilience-challenge-scenarios-b.js +15 -20
  43. package/dist/tools/challenge-spec/scenario-collector.d.ts +13 -0
  44. package/dist/tools/challenge-spec/scenario-collector.js +86 -0
  45. package/dist/tools/challenge-spec/scenarios-data.js +10 -9
  46. package/dist/tools/challenge-spec/scenarios-failure.js +38 -54
  47. package/dist/tools/challenge-spec/scenarios-scale.js +14 -15
  48. package/dist/tools/challenge-spec/scenarios-security.d.ts +1 -1
  49. package/dist/tools/challenge-spec/scenarios-security.js +18 -11
  50. package/dist/tools/challenge-spec/scenarios-utils.d.ts +31 -5
  51. package/dist/tools/challenge-spec/scenarios-utils.js +102 -6
  52. package/dist/tools/challenge-spec/security-challenge-scenarios.d.ts +1 -1
  53. package/dist/tools/challenge-spec/security-challenge-scenarios.js +20 -25
  54. package/dist/tools/challenge-spec.js +39 -118
  55. package/dist/tools/compliance-gate-handler.d.ts +1 -1
  56. package/dist/tools/create-spec.js +4 -24
  57. package/dist/tools/delete-first/suggest-deletions.d.ts +1 -1
  58. package/dist/tools/generate-batch-script.d.ts +2 -2
  59. package/dist/tools/init-project/rules-generator.js +0 -1
  60. package/dist/tools/oauth-handler.d.ts +1 -1
  61. package/dist/tools/register-sdd-tools.d.ts +1 -1
  62. package/dist/tools/render-spec-for-provider.d.ts +2 -2
  63. package/dist/tools/scan-orphan-spec-refs.d.ts +6 -6
  64. package/dist/tools/schemas/agents.d.ts +7 -7
  65. package/dist/tools/schemas/analysis.d.ts +11 -11
  66. package/dist/tools/schemas/delete.d.ts +5 -5
  67. package/dist/tools/schemas/github.d.ts +4 -4
  68. package/dist/tools/schemas/ide-config.d.ts +12 -12
  69. package/dist/tools/schemas/infra.d.ts +9 -9
  70. package/dist/tools/schemas/lifecycle.d.ts +50 -50
  71. package/dist/tools/schemas/llm-provider-schemas.d.ts +7 -7
  72. package/dist/tools/schemas/plugins-schemas.d.ts +12 -12
  73. package/dist/tools/schemas/project-graph.d.ts +6 -6
  74. package/dist/tools/schemas/rbac.d.ts +2 -2
  75. package/dist/tools/schemas/registry.d.ts +3 -3
  76. package/dist/tools/schemas/runtime-security.d.ts +2 -2
  77. package/dist/tools/schemas/session.d.ts +2 -2
  78. package/dist/tools/schemas/spec.d.ts +15 -15
  79. package/dist/tools/schemas/token-intelligence.d.ts +6 -6
  80. package/dist/tools/schemas/token-optimization.d.ts +2 -2
  81. package/dist/tools/schemas/workers-schema.d.ts +4 -4
  82. package/dist/tools/skill-bootstrap-handler.d.ts +7 -7
  83. package/dist/tools/tool-registry/deprecated-stubs.d.ts +1 -1
  84. package/dist/tools/triage-request.d.ts +3 -3
  85. package/dist/tools/update-status/file-sync.d.ts +1 -1
  86. package/dist/tools/update-status/file-sync.js +10 -45
  87. package/dist/tools/update-status/index.js +28 -27
  88. package/dist/tools/update-status/response-builder.d.ts +1 -1
  89. package/dist/tools/update-status/response-builder.js +11 -19
  90. package/dist/tools/update-status/transition-guard.js +1 -1
  91. package/dist/tools/update-status-actions.d.ts +0 -1
  92. package/dist/tools/update-status-actions.js +10 -26
  93. package/dist/tools/update-status-convention-gate.d.ts +1 -6
  94. package/dist/tools/update-status-convention-gate.js +9 -33
  95. package/dist/types/docs.d.ts +0 -4
  96. package/dist/types/env.d.ts +0 -4
  97. package/dist/types/estimation.d.ts +0 -2
  98. package/dist/types/multi-teammate-review.d.ts +4 -4
  99. package/dist/types/plugin-install.d.ts +7 -7
  100. package/package.json +21 -18
  101. package/planu-native.json +1 -1
  102. package/planu-plugin.json +1 -1
  103. package/src/i18n/messages/en.json +0 -72
  104. package/src/i18n/messages/es.json +0 -72
  105. package/src/i18n/messages/pt.json +0 -72
  106. package/dist/config/elicitation-dimensions.json +0 -51
  107. package/dist/engine/actuals-estimator.d.ts +0 -14
  108. package/dist/engine/actuals-estimator.js +0 -64
  109. package/dist/engine/cascade-hooks/hooks/living-specs.hook.d.ts +0 -3
  110. package/dist/engine/cascade-hooks/hooks/living-specs.hook.js +0 -19
  111. package/dist/engine/elicitation/option-builder.d.ts +0 -9
  112. package/dist/engine/elicitation/option-builder.js +0 -85
  113. package/dist/engine/progress-writer.d.ts +0 -10
  114. package/dist/engine/progress-writer.js +0 -63
  115. package/dist/tools/update-status/mode-hints.d.ts +0 -4
  116. package/dist/tools/update-status/mode-hints.js +0 -24
@@ -2,11 +2,11 @@ import { z } from 'zod';
2
2
  export declare const SecurityReportTimeRangeEnum: z.ZodEnum<{
3
3
  all: "all";
4
4
  last24h: "last24h";
5
- last7d: "last7d";
6
5
  last30d: "last30d";
6
+ last7d: "last7d";
7
7
  }>;
8
8
  export declare const SecurityReportFormatEnum: z.ZodEnum<{
9
- summary: "summary";
10
9
  detailed: "detailed";
10
+ summary: "summary";
11
11
  }>;
12
12
  //# sourceMappingURL=runtime-security.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  export declare const SessionFormatEnum: z.ZodEnum<{
3
- markdown: "markdown";
4
3
  json: "json";
4
+ markdown: "markdown";
5
5
  }>;
6
6
  export declare const SessionHandoffSchema: {
7
7
  readonly projectPath: z.ZodString;
@@ -19,8 +19,8 @@ export declare const ExportSessionSchema: {
19
19
  readonly projectPath: z.ZodString;
20
20
  readonly sessionId: z.ZodOptional<z.ZodString>;
21
21
  readonly format: z.ZodOptional<z.ZodEnum<{
22
- markdown: "markdown";
23
22
  json: "json";
23
+ markdown: "markdown";
24
24
  }>>;
25
25
  };
26
26
  export declare const AutoCheckpointSchema: {
@@ -5,43 +5,43 @@ export declare const SupportedLocaleEnum: z.ZodEnum<{
5
5
  pt: "pt";
6
6
  }>;
7
7
  export declare const SpecStatusEnum: z.ZodEnum<{
8
- draft: "draft";
9
- review: "review";
10
8
  approved: "approved";
11
- implementing: "implementing";
12
- done: "done";
13
9
  discarded: "discarded";
10
+ done: "done";
11
+ draft: "draft";
12
+ implementing: "implementing";
13
+ review: "review";
14
14
  }>;
15
15
  export declare const SpecTypeEnum: z.ZodEnum<{
16
- feature: "feature";
17
- refactor: "refactor";
16
+ agent: "agent";
18
17
  bugfix: "bugfix";
19
- infra: "infra";
20
18
  docs: "docs";
19
+ feature: "feature";
20
+ infra: "infra";
21
21
  project: "project";
22
- agent: "agent";
22
+ refactor: "refactor";
23
23
  "tech-debt": "tech-debt";
24
24
  }>;
25
25
  export declare const SpecScopeEnum: z.ZodEnum<{
26
+ architectural: "architectural";
27
+ "cross-module": "cross-module";
26
28
  feature: "feature";
27
29
  trivial: "trivial";
28
- "cross-module": "cross-module";
29
- architectural: "architectural";
30
30
  }>;
31
31
  export declare const SpecTargetEnum: z.ZodEnum<{
32
- frontend: "frontend";
32
+ android: "android";
33
33
  backend: "backend";
34
- shared: "shared";
34
+ database: "database";
35
+ frontend: "frontend";
35
36
  fullstack: "fullstack";
36
37
  infrastructure: "infrastructure";
37
- database: "database";
38
38
  ios: "ios";
39
- android: "android";
39
+ shared: "shared";
40
40
  }>;
41
41
  export declare const ExperienceLevelEnum: z.ZodEnum<{
42
42
  beginner: "beginner";
43
- intermediate: "intermediate";
44
43
  expert: "expert";
44
+ intermediate: "intermediate";
45
45
  }>;
46
46
  /** SPEC-313: Work mode enum for init_project and set_work_mode */
47
47
  export declare const WorkModeEnum: z.ZodEnum<{
@@ -1,23 +1,23 @@
1
1
  import { z } from 'zod';
2
2
  export declare const TokenIntelligencePeriodEnum: z.ZodEnum<{
3
+ all_time: "all_time";
4
+ month: "month";
3
5
  today: "today";
4
6
  week: "week";
5
- month: "month";
6
- all_time: "all_time";
7
7
  }>;
8
8
  export declare const TokenIntelligenceGroupByEnum: z.ZodEnum<{
9
+ day: "day";
9
10
  model: "model";
10
11
  spec: "spec";
11
12
  tool: "tool";
12
- day: "day";
13
13
  }>;
14
14
  export declare const TokenIntelligenceViewEnum: z.ZodEnum<{
15
- graph: "graph";
16
- summary: "summary";
15
+ autopilot: "autopilot";
17
16
  budget: "budget";
18
17
  detailed: "detailed";
19
- autopilot: "autopilot";
18
+ graph: "graph";
20
19
  reconciliation: "reconciliation";
20
+ summary: "summary";
21
21
  trends: "trends";
22
22
  }>;
23
23
  //# sourceMappingURL=token-intelligence.d.ts.map
@@ -1,9 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  export declare const TokenUsagePeriodEnum: z.ZodEnum<{
3
- today: "today";
3
+ all_time: "all_time";
4
4
  current_session: "current_session";
5
5
  last_hour: "last_hour";
6
- all_time: "all_time";
6
+ today: "today";
7
7
  }>;
8
8
  export declare const TokenUsageGroupByEnum: z.ZodEnum<{
9
9
  session: "session";
@@ -8,13 +8,13 @@ export declare const ConfigureWorkersInputSchema: z.ZodObject<{
8
8
  projectId: z.ZodString;
9
9
  workerName: z.ZodString;
10
10
  action: z.ZodEnum<{
11
- enable: "enable";
11
+ "add-pattern": "add-pattern";
12
12
  disable: "disable";
13
+ enable: "enable";
14
+ "enable-auto-memory": "enable-auto-memory";
15
+ "remove-pattern": "remove-pattern";
13
16
  "set-cooldown": "set-cooldown";
14
17
  "set-priority": "set-priority";
15
- "add-pattern": "add-pattern";
16
- "remove-pattern": "remove-pattern";
17
- "enable-auto-memory": "enable-auto-memory";
18
18
  }>;
19
19
  value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
20
20
  }, z.core.$strip>;
@@ -5,20 +5,20 @@ export declare const DiscoverSkillsInputSchema: {
5
5
  stack: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
6
  sources: z.ZodOptional<z.ZodArray<z.ZodEnum<{
7
7
  "awesome-cursorrules": "awesome-cursorrules";
8
- "skills-sh": "skills-sh";
9
8
  "github-custom": "github-custom";
9
+ "skills-sh": "skills-sh";
10
10
  }>>>;
11
11
  };
12
12
  export declare const BootstrapSkillsInputSchema: {
13
13
  projectPath: z.ZodString;
14
14
  stack: z.ZodOptional<z.ZodArray<z.ZodString>>;
15
15
  targets: z.ZodOptional<z.ZodArray<z.ZodEnum<{
16
+ "agents-md": "agents-md";
16
17
  "claude-md": "claude-md";
18
+ clinerules: "clinerules";
17
19
  cursorrules: "cursorrules";
18
- windsurfrules: "windsurfrules";
19
20
  "kiro-steering": "kiro-steering";
20
- "agents-md": "agents-md";
21
- clinerules: "clinerules";
21
+ windsurfrules: "windsurfrules";
22
22
  }>>>;
23
23
  dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
24
24
  };
@@ -34,12 +34,12 @@ export declare const ConfigureSkillRegistriesInputSchema: {
34
34
  enableSource: z.ZodOptional<z.ZodString>;
35
35
  disableSource: z.ZodOptional<z.ZodString>;
36
36
  setTargets: z.ZodOptional<z.ZodArray<z.ZodEnum<{
37
+ "agents-md": "agents-md";
37
38
  "claude-md": "claude-md";
39
+ clinerules: "clinerules";
38
40
  cursorrules: "cursorrules";
39
- windsurfrules: "windsurfrules";
40
41
  "kiro-steering": "kiro-steering";
41
- "agents-md": "agents-md";
42
- clinerules: "clinerules";
42
+ windsurfrules: "windsurfrules";
43
43
  }>>>;
44
44
  setAutoMerge: z.ZodOptional<z.ZodBoolean>;
45
45
  };
@@ -5,7 +5,7 @@ export declare const DEPRECATION_MESSAGE_TEMPLATE = "This tool has been migrated
5
5
  * The 5 MCP tool names that have been migrated to skills.
6
6
  * Used in smoke tests (Scenario 6) and anywhere a guard on migrated tools is needed.
7
7
  */
8
- export declare const DEPRECATED_TOOLS: readonly ["execute_sdd_flow", "implement_plan", "multi_teammate_review", "run_healing_loop", "generate_orchestration_plan"];
8
+ export declare const DEPRECATED_TOOLS: readonly ['execute_sdd_flow', 'implement_plan', 'multi_teammate_review', 'run_healing_loop', 'generate_orchestration_plan'];
9
9
  export type DeprecatedToolName = (typeof DEPRECATED_TOOLS)[number];
10
10
  /**
11
11
  * Create a ToolEntry-compatible deprecation stub.
@@ -5,11 +5,11 @@ export declare const TriageRequestInputSchema: z.ZodObject<{
5
5
  sessionId: z.ZodOptional<z.ZodString>;
6
6
  hint: z.ZodOptional<z.ZodObject<{
7
7
  kind: z.ZodOptional<z.ZodEnum<{
8
- idea: "idea";
9
- "quick-fix": "quick-fix";
10
8
  "bug-spec": "bug-spec";
11
- "feature-spec": "feature-spec";
12
9
  epic: "epic";
10
+ "feature-spec": "feature-spec";
11
+ idea: "idea";
12
+ "quick-fix": "quick-fix";
13
13
  }>>;
14
14
  }, z.core.$strip>>;
15
15
  projectId: z.ZodOptional<z.ZodString>;
@@ -10,7 +10,7 @@ export interface FrontmatterSyncWarning {
10
10
  specPath: string | null;
11
11
  reason: string;
12
12
  }
13
- export declare function syncSpecFiles(updatedSpec: Spec, currentStatus: SpecStatus, newStatus: SpecStatus, projectPath?: string): Promise<{
13
+ export declare function syncSpecFiles(updatedSpec: Spec, _currentStatus: SpecStatus, newStatus: SpecStatus, projectPath?: string): Promise<{
14
14
  warning?: FrontmatterSyncWarning;
15
15
  }>;
16
16
  export declare function tryReconcile(newStatus: string, specId: string, projectId: string): Promise<string | null>;
@@ -2,14 +2,15 @@ import { calculateAccuracy } from '../../engine/estimator.js';
2
2
  import { metricsStore } from '../../storage/index.js';
3
3
  import { addLesson, getLessons } from '../../storage/lessons-store.js';
4
4
  import { dispatchFeedbackEvent } from '../learn.js';
5
- import { generateProgressUpdate } from '../../engine/progress-writer.js';
6
5
  import { autoReconcileOnDone } from '../update-status-reconcile.js';
7
6
  import { readFile, writeFile } from 'node:fs/promises';
8
- import { replaceSectionInSpec } from '../../engine/spec-format/replace-section.js';
9
7
  import { parseFrontmatter } from '../../engine/frontmatter-parser.js';
10
8
  import { computeFrontmatterSha } from '../../engine/frontmatter-sha/index.js';
11
9
  import { appendTransitionEvent } from '../../storage/transition-log.js';
12
10
  export async function recordDoneMetrics(projectId, specId, spec, actuals, projectPath) {
11
+ if (actuals.devHours + actuals.reviewHours === 0) {
12
+ return;
13
+ }
13
14
  const accuracyScore = calculateAccuracy(spec.estimation, actuals.devHours + actuals.reviewHours);
14
15
  const metricEntry = {
15
16
  specId,
@@ -19,7 +20,7 @@ export async function recordDoneMetrics(projectId, specId, spec, actuals, projec
19
20
  accuracyScore,
20
21
  };
21
22
  await metricsStore.addMetricEntry(projectId, metricEntry);
22
- if (accuracyScore < 0.7 || accuracyScore > 1.3) {
23
+ if (accuracyScore < 70) {
23
24
  void dispatchFeedbackEvent(projectId, {
24
25
  type: 'validate',
25
26
  specId,
@@ -29,13 +30,13 @@ export async function recordDoneMetrics(projectId, specId, spec, actuals, projec
29
30
  estimatedHours: spec.estimation.devHours + spec.estimation.reviewHours,
30
31
  actualHours: actuals.devHours + actuals.reviewHours,
31
32
  },
32
- delta: { accuracyDrift: Math.abs(1 - accuracyScore) },
33
+ delta: { accuracyDrift: Math.abs(100 - accuracyScore) },
33
34
  timestamp: new Date().toISOString(),
34
35
  }).catch((err) => {
35
36
  console.error('[planu] dispatchFeedbackEvent failed:', err);
36
37
  });
37
38
  }
38
- if (accuracyScore < 0.7 && projectPath) {
39
+ if (accuracyScore < 70 && projectPath) {
39
40
  void autoCaptureLessonEstimation(spec, actuals, accuracyScore, projectId, projectPath);
40
41
  }
41
42
  }
@@ -51,14 +52,14 @@ async function autoCaptureLessonEstimation(spec, actuals, accuracy, projectId, p
51
52
  }
52
53
  const estimatedHours = spec.estimation.devHours + spec.estimation.reviewHours;
53
54
  const actualHours = actuals.devHours + actuals.reviewHours;
54
- const severity = accuracy < 0.6 ? 'high' : 'medium';
55
+ const severity = accuracy < 60 ? 'high' : 'medium';
55
56
  await addLesson(projectPath, {
56
57
  projectId,
57
58
  specId: spec.id,
58
59
  category: 'estimation',
59
60
  severity,
60
61
  title: `Estimation miss on ${spec.title}`,
61
- description: `Spec took ${String(actualHours)}h actual vs ${String(estimatedHours)}h estimated (${String(Math.round(accuracy * 100))}% accuracy)`,
62
+ description: `Spec took ${String(actualHours)}h actual vs ${String(estimatedHours)}h estimated (${String(Math.round(accuracy))}% accuracy)`,
62
63
  prevention: 'Review complexity and scope assumptions before estimating similar specs',
63
64
  tags: [...spec.tags, 'auto-captured', 'estimation'],
64
65
  });
@@ -90,7 +91,7 @@ function assertSpecBodyIntegrity(args) {
90
91
  throw new Error(`SPEC_INTEGRITY_WRITE_FAILED: refusing to write ${args.specId} because update_status(${args.newStatus}) would shrink spec.md body by ${String(lost)} characters without an explicit migration marker.`);
91
92
  }
92
93
  }
93
- export async function syncSpecFiles(updatedSpec, currentStatus, newStatus, projectPath) {
94
+ export async function syncSpecFiles(updatedSpec, _currentStatus, newStatus, projectPath) {
94
95
  let warning;
95
96
  if (updatedSpec.specPath) {
96
97
  try {
@@ -145,21 +146,6 @@ export async function syncSpecFiles(updatedSpec, currentStatus, newStatus, proje
145
146
  console.warn(`[Planu] update_status: ${reason} (${updatedSpec.id})`);
146
147
  warning = { specId: updatedSpec.id, specPath: null, reason };
147
148
  }
148
- // SPEC-1010 PR-C: Write progress into the unified spec.md ## Progress section
149
- // instead of a standalone progress.md file. We do this only when specPath is
150
- // known (which it always is for specs that reach syncSpecFiles).
151
- if (updatedSpec.specPath) {
152
- try {
153
- const existing = await readFile(updatedSpec.specPath, 'utf-8');
154
- // Extract current ## Progress body (may be empty for new lean specs)
155
- const progressBody = extractProgressBody(existing);
156
- const updatedProgress = generateProgressUpdate(updatedSpec, currentStatus, progressBody);
157
- await replaceSectionInSpec(updatedSpec.specPath, 'Progress', updatedProgress);
158
- }
159
- catch {
160
- // best-effort — never blocks the transition
161
- }
162
- }
163
149
  return warning ? { warning } : {};
164
150
  }
165
151
  /** Flip all ` done: false` lines to ` done: true` in spec content.
@@ -169,7 +155,7 @@ function markAllCriteriaDone(content) {
169
155
  }
170
156
  /** True when content is a lean-format spec (has a `criteria:` key in the frontmatter). */
171
157
  function isLeanSpecContent(content) {
172
- return /^criteria:/m.test(content);
158
+ return /^criteria:[ \t]*(?:#.*)?\r?\n(?:[ \t]*\r?\n)*[ \t]+-\s+text:/m.test(content);
173
159
  }
174
160
  /**
175
161
  * SPEC-630: Remove done criteria entries from lean spec and annotate the count on the criteria key.
@@ -183,27 +169,6 @@ function stripDoneCriteria(content) {
183
169
  });
184
170
  return stripped.replace(/^(criteria:)(\s*#.*)?$/m, `$1 # ${String(doneCount)} done`);
185
171
  }
186
- /**
187
- * Extract the body of the `## Progress` section from spec.md content.
188
- * Returns "" when the section is absent so callers get a clean empty string.
189
- * Mirrors the extractSectionBody logic without importing from spec-format to
190
- * keep this module's dependency chain flat.
191
- */
192
- function extractProgressBody(specContent) {
193
- const normalized = specContent.replace(/\r\n/g, '\n');
194
- // Mask fenced code blocks so inner markdown headings are not matched
195
- const masked = normalized.replace(/(```|~~~)[\s\S]*?\1/g, (block) => block.replace(/[^\n]/g, ' '));
196
- const headingMatch = /^##\s+Progress\b.*$/m.exec(masked);
197
- if (!headingMatch) {
198
- return '';
199
- }
200
- const afterHeading = headingMatch.index + headingMatch[0].length;
201
- const nextRe = /^##\s+\S/gm;
202
- nextRe.lastIndex = afterHeading;
203
- const next = nextRe.exec(masked);
204
- const end = next ? next.index : normalized.length;
205
- return normalized.slice(afterHeading, end).trim();
206
- }
207
172
  export async function tryReconcile(newStatus, specId, projectId) {
208
173
  if (newStatus !== 'done') {
209
174
  return null;
@@ -16,8 +16,6 @@ import { isLocked, getLock } from '../../storage/spec-lock-store.js';
16
16
  import { runValidateGate, checkDoneGates, checkComplianceGate, checkQaGate, checkApprovedFormatGate, checkValidationReportGate, checkSpecReviewGate, writeSpecReviewArtifact, } from './dod-gates.js';
17
17
  import { checkLifecycleEvidenceTransitionGate } from './evidence-gate.js';
18
18
  import { buildStatusResponse, buildValidateBlockedResponse, buildDryRunResponse, } from './response-builder.js';
19
- import { getSuggestedMode } from './mode-hints.js';
20
- import { autoFillActuals, createDefaultActuals } from '../../engine/actuals-estimator.js';
21
19
  import { recordDoneMetrics, syncSpecFiles, tryReconcile, recordTerminalTransitionEvent, } from './file-sync.js';
22
20
  import { appendEntry, getLastHash } from '../../storage/audit-trail-store.js';
23
21
  import { appendTransitionEvent } from '../../storage/transition-log.js';
@@ -117,18 +115,18 @@ function computeDoneMetrics(newStatus, spec, actuals) {
117
115
  if (newStatus !== 'done' || actuals === undefined || spec === null) {
118
116
  return null;
119
117
  }
118
+ const actualHours = actuals.devHours + actuals.reviewHours;
119
+ if (actualHours === 0) {
120
+ return null;
121
+ }
120
122
  return {
121
- accuracyScore: calculateAccuracy(spec.estimation, actuals.devHours + actuals.reviewHours),
123
+ accuracyScore: calculateAccuracy(spec.estimation, actualHours),
122
124
  estimatedHours: spec.estimation.devHours + spec.estimation.reviewHours,
123
- actualHours: actuals.devHours + actuals.reviewHours,
125
+ actualHours,
124
126
  estimatedCost: spec.estimation.totalCostUsd,
125
127
  actualCost: actuals.totalCostUsd,
126
128
  };
127
129
  }
128
- /** Extract recordActualHint from doneActions without adding cyclomatic complexity inline. */
129
- function extractRecordActualHint(doneActions) {
130
- return doneActions?.recordActualHint;
131
- }
132
130
  /** SPEC-447: Build a blocked-by-compliance response. */
133
131
  function buildBlockedByComplianceResponse(_specId, result) {
134
132
  const scoreStr = result.score !== null ? String(result.score) : 'N/A';
@@ -269,6 +267,22 @@ export async function handleUpdateStatus(params, server) {
269
267
  };
270
268
  }
271
269
  const originalStatus = spec.status;
270
+ if (newStatus === 'done' && actuals === undefined) {
271
+ return {
272
+ content: [
273
+ {
274
+ type: 'text',
275
+ text: 'actuals_required: provide measured values for done. Use zero only when a metric is explicitly unavailable; Planu does not estimate provider tokens or costs.',
276
+ },
277
+ ],
278
+ isError: true,
279
+ structuredContent: {
280
+ error: 'actuals_required',
281
+ code: 422,
282
+ fixHint: 'Provide actuals with measured values or zero for unavailable metrics. Values are preserved without estimation.',
283
+ },
284
+ };
285
+ }
272
286
  // SPEC-301: Reject status changes when spec is locked by another holder (legacy manual lock)
273
287
  const lockError = await checkSpecLockGate(params.projectPath, specId);
274
288
  if (lockError) {
@@ -477,7 +491,7 @@ export async function handleUpdateStatus(params, server) {
477
491
  let forcedBypassWarning = null;
478
492
  const [codeRealityResult, doneGatesResult] = await Promise.all([
479
493
  // Code reality: only relevant for 'implementing'
480
- newStatus === 'implementing' && effectiveGatePath
494
+ newStatus === 'implementing' && !reverseTransition && effectiveGatePath
481
495
  ? checkCodeReality(effectiveGatePath, spec.title).catch(() => null)
482
496
  : Promise.resolve(null),
483
497
  // Done gates: only relevant for 'done'
@@ -521,8 +535,8 @@ export async function handleUpdateStatus(params, server) {
521
535
  newStatus === 'done' && effectiveGatePath && !crashShieldSkipReason
522
536
  ? withToolTimeout(scanCrashRisks(effectiveGatePath).catch(() => null), 9_000, null)
523
537
  : Promise.resolve(null),
524
- // Compliance gate: only on 'done' or 'implementing' (score-compliance is heavy) hard deadline 9s
525
- newStatus === 'done' || newStatus === 'implementing' || newStatus === 'review'
538
+ // The heuristic scorer is a review aid. Done relies on the authoritative validate report.
539
+ newStatus === 'review'
526
540
  ? withToolTimeout(checkComplianceGate(specId, projectId, effectiveGatePath), 9_000, {
527
541
  skipped: true,
528
542
  blocked: false,
@@ -631,18 +645,8 @@ export async function handleUpdateStatus(params, server) {
631
645
  blockingReasons: [],
632
646
  });
633
647
  }
634
- // SPEC-495: Auto-generate actuals when not provided; auto-fill zeros when provided
635
- let resolvedActuals = actuals;
636
- if (newStatus === 'done') {
637
- if (!resolvedActuals) {
638
- // Agent did not pass actuals at all — create from spec estimation
639
- resolvedActuals = createDefaultActuals(spec.estimation);
640
- }
641
- else {
642
- // Agent passed actuals but tokens/costs are zero — auto-fill
643
- resolvedActuals = autoFillActuals(resolvedActuals, spec.estimation);
644
- }
645
- }
648
+ // Explicit actuals are preserved verbatim. Provider usage and cost are never inferred.
649
+ const resolvedActuals = actuals;
646
650
  // Run transition-specific actions
647
651
  const implActions = newStatus === 'implementing' ? await runImplementingActions(projectId, specId) : null;
648
652
  const doneActions = newStatus === 'done' ? await runDoneActions(projectId, specId, spec.gitBranch) : null;
@@ -1030,7 +1034,6 @@ export async function handleUpdateStatus(params, server) {
1030
1034
  if (cascadeResult.affectedSpecs.length > 0) {
1031
1035
  collector.pushOk('cascade', `Notified ${String(cascadeResult.affectedSpecs.length)} downstream spec(s)`);
1032
1036
  }
1033
- const modeSuggestion = getSuggestedMode(newStatus);
1034
1037
  const result = compactObj({
1035
1038
  specId,
1036
1039
  projectId,
@@ -1068,8 +1071,6 @@ export async function handleUpdateStatus(params, server) {
1068
1071
  metrics: doneMetrics,
1069
1072
  complianceGateResult,
1070
1073
  versionSnapshotTag,
1071
- suggestedClaudeMode: modeSuggestion?.mode ?? null,
1072
- modeHint: modeSuggestion?.hint ?? null,
1073
1074
  orchestrationPlan,
1074
1075
  // SPEC-728: DepGuard result (null when gate was not applicable)
1075
1076
  depGuard: depGuardResult,
@@ -1105,7 +1106,7 @@ export async function handleUpdateStatus(params, server) {
1105
1106
  const reconciliationMarkdown = await tryReconcile(newStatus, specId, projectId);
1106
1107
  // SPEC-754: Append shell-hygiene reminder when transitioning to done on claude-code host
1107
1108
  const shellHygieneHint = newStatus === 'done' && detectHost() === 'claude-code' ? shellHygieneReminder() : null;
1108
- return buildStatusResponse(result, specId, currentStatus, newStatus, reconciliationMarkdown, extractRecordActualHint(doneActions), spec.title, shellHygieneHint);
1109
+ return buildStatusResponse(result, specId, currentStatus, newStatus, reconciliationMarkdown, spec.title, shellHygieneHint);
1109
1110
  }
1110
1111
  finally {
1111
1112
  // SPEC-719: Release cross-process lock in all exit paths (return, throw)
@@ -13,5 +13,5 @@ export declare function buildValidateBlockedResponse(specId: string, result: Ext
13
13
  */
14
14
  export declare function buildDryRunResponse(dryRunResult: DryRunResult): ToolResult;
15
15
  export declare const STATUS_EMOJIS: Record<SpecStatus, string>;
16
- export declare function buildStatusResponse(result: Record<string, unknown>, specId: string, currentStatus: SpecStatus, newStatus: SpecStatus, reconciliationMarkdown?: string | null, recordActualHint?: string | null, specTitle?: string, shellHygieneHint?: string | null): ToolResult;
16
+ export declare function buildStatusResponse(result: Record<string, unknown>, specId: string, currentStatus: SpecStatus, newStatus: SpecStatus, reconciliationMarkdown?: string | null, specTitle?: string, shellHygieneHint?: string | null): ToolResult;
17
17
  //# sourceMappingURL=response-builder.d.ts.map
@@ -79,7 +79,7 @@ export const STATUS_EMOJIS = {
79
79
  discarded: '🗑️',
80
80
  };
81
81
  // eslint-disable-next-line complexity -- aggregates many host- and status-specific branches
82
- export function buildStatusResponse(result, specId, currentStatus, newStatus, reconciliationMarkdown, recordActualHint, specTitle, shellHygieneHint) {
82
+ export function buildStatusResponse(result, specId, currentStatus, newStatus, reconciliationMarkdown, specTitle, shellHygieneHint) {
83
83
  const fromEmoji = STATUS_EMOJIS[currentStatus];
84
84
  const toEmoji = STATUS_EMOJIS[newStatus];
85
85
  const r = result;
@@ -102,18 +102,15 @@ export function buildStatusResponse(result, specId, currentStatus, newStatus, re
102
102
  lines.push(`\n🔗 ${r.prSuggestion.title}`);
103
103
  }
104
104
  if (r.metrics) {
105
- lines.push(`\n📊 Accuracy: ${Math.round(r.metrics.accuracyScore * 100)}% | ` +
105
+ lines.push(`\n📊 Accuracy: ${Math.round(r.metrics.accuracyScore)}% | ` +
106
106
  `Estimated: ${String(r.metrics.estimatedHours)}h → Actual: ${String(r.metrics.actualHours)}h`);
107
107
  }
108
108
  if (r.actuals && (r.actuals.tokensOpus > 0 || r.actuals.tokensSonnet > 0)) {
109
- const prefix = r.actuals.estimated ? '~' : '';
110
- const opus = r.actuals.tokensOpus > 0 ? `${prefix}${r.actuals.tokensOpus.toLocaleString()} opus` : null;
111
- const sonnet = r.actuals.tokensSonnet > 0
112
- ? `${prefix}${r.actuals.tokensSonnet.toLocaleString()} sonnet`
113
- : null;
109
+ const opus = r.actuals.tokensOpus > 0 ? `${r.actuals.tokensOpus.toLocaleString()} opus` : null;
110
+ const sonnet = r.actuals.tokensSonnet > 0 ? `${r.actuals.tokensSonnet.toLocaleString()} sonnet` : null;
114
111
  const tokenStr = [opus, sonnet].filter(Boolean).join(' + ');
115
- const costStr = r.actuals.apiCostUsd > 0 ? ` | ${prefix}$${r.actuals.apiCostUsd.toFixed(3)} API` : '';
116
- lines.push(`\n🤖 Tokens: ${tokenStr}${costStr}${r.actuals.estimated ? ' *(auto-estimated)*' : ''}`);
112
+ const costStr = r.actuals.apiCostUsd > 0 ? ` | $${r.actuals.apiCostUsd.toFixed(3)} API` : '';
113
+ lines.push(`\n🤖 Tokens: ${tokenStr}${costStr}`);
117
114
  }
118
115
  if (r.validateScore !== null && r.validateScore !== undefined) {
119
116
  const sourceSuffix = r.validateScoreSource === 'forced-best-effort-validateSpec'
@@ -180,22 +177,17 @@ export function buildStatusResponse(result, specId, currentStatus, newStatus, re
180
177
  const v = r.orchestrationPlan.parallelSafetyVerdict ? 'parallel-safe' : 'sequential-required';
181
178
  lines.push(`\n🤖 Orchestration: ${String(r.orchestrationPlan.executionOrder.length)} waves | ${v}`);
182
179
  }
183
- // SPEC-494: Claude Code mode hint
184
- if (r.suggestedClaudeMode) {
185
- lines.push(`\n💡 **Suggested Claude mode:** \`${r.suggestedClaudeMode}\` — ${r.modeHint ?? ''}`);
186
- }
187
- if (recordActualHint) {
188
- lines.push(`\n💡 ${recordActualHint}`);
189
- }
190
180
  // SPEC-754: Shell hygiene reminder for claude-code host on done transition
191
181
  if (shellHygieneHint) {
192
182
  lines.push(`\n🐚 **Shell hygiene:** ${shellHygieneHint}`);
193
183
  }
194
184
  // SPEC-340: nextAction and lifecycleMap
195
- const nextAction = buildNextAction(newStatus);
196
185
  const lifecycleMap = buildLifecycleMap(newStatus);
186
+ const nextAction = lifecycleMap.next === null ? null : buildNextAction(newStatus);
197
187
  lines.push(buildLifecycleMapLine(lifecycleMap));
198
- lines.push(`\n**Next step:** ${nextAction.step}\n> ${nextAction.prompt}\n> \`${nextAction.suggestedCommand}\` — ${nextAction.whenToDoIt}`);
188
+ if (nextAction) {
189
+ lines.push(`\n**Next step:** ${nextAction.step}\n> ${nextAction.prompt}\n> \`${nextAction.suggestedCommand}\` — ${nextAction.whenToDoIt}`);
190
+ }
199
191
  const allNextSteps = [...(r.implementingSuggestions ?? [])];
200
192
  const baseText = formatSuccess(t('tools.response.statusTitle'), lines.join(''));
201
193
  const withNextSteps = allNextSteps.length > 0 ? addNextSteps(baseText, allNextSteps) : baseText;
@@ -284,7 +276,7 @@ export function buildStatusResponse(result, specId, currentStatus, newStatus, re
284
276
  warningsTop5: warningItems.slice(0, 5),
285
277
  affectedSpecsCount: r.cascade.affectedSpecs.length,
286
278
  affectedSpecsTop5: r.cascade.affectedSpecs.slice(0, 5),
287
- nextAction,
279
+ nextAction: nextAction ?? undefined,
288
280
  lifecycleMap,
289
281
  humanSummary,
290
282
  orchestrationPlan: r.orchestrationPlan
@@ -115,7 +115,7 @@ export function checkTransition(currentStatus, newStatus) {
115
115
  * Returns an error ToolResult if blocked, null if passed.
116
116
  */
117
117
  export function checkDorGate(spec, specId, projectId, newStatus) {
118
- if (newStatus !== 'implementing') {
118
+ if (newStatus !== 'implementing' || isReverseTransition(spec.status, newStatus)) {
119
119
  return null;
120
120
  }
121
121
  const dorResult = validateDoR(spec);
@@ -13,7 +13,6 @@ export declare function runDoneActions(projectId: string, specId: string, gitBra
13
13
  } | null;
14
14
  validationWarning: string | null;
15
15
  uncommittedWarning: string | null;
16
- recordActualHint: string | null;
17
16
  autopilotSummary: string[];
18
17
  }>;
19
18
  /**