@planu/cli 4.11.1 → 4.11.3

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 (155) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/config/elicitation-questions.json +5 -17
  3. package/dist/engine/cascade-hooks/registry.js +0 -2
  4. package/dist/engine/challenge-scenarios-loader.d.ts +3 -3
  5. package/dist/engine/compliance-injector.d.ts +3 -3
  6. package/dist/engine/dashboard/kanban-logic.d.ts +1 -1
  7. package/dist/engine/doc-generator/portal/risk-matrix-svg.d.ts +1 -1
  8. package/dist/engine/doc-generator/proposal/proposal-generator.d.ts +1 -1
  9. package/dist/engine/elicitation/answer-extractor.d.ts +1 -4
  10. package/dist/engine/elicitation/answer-extractor.js +197 -68
  11. package/dist/engine/elicitation/decision-gap-detector.js +59 -79
  12. package/dist/engine/elicitation/index.d.ts +0 -1
  13. package/dist/engine/elicitation/index.js +0 -1
  14. package/dist/engine/elicitation/question-generator.js +10 -3
  15. package/dist/engine/elicitation/question-grounding-gate.js +83 -34
  16. package/dist/engine/evidence-gates/lifecycle-gate.js +27 -14
  17. package/dist/engine/evidence-index/index-builder.js +7 -2
  18. package/dist/engine/frontmatter-sha/frozen-fields.d.ts +1 -1
  19. package/dist/engine/handoff-artifacts/schemas/red-tests-handoff.d.ts +2 -2
  20. package/dist/engine/handoff-artifacts/schemas.d.ts +22 -22
  21. package/dist/engine/human-summary.js +3 -3
  22. package/dist/engine/implementation-contract/common.d.ts +1 -1
  23. package/dist/engine/implementation-contract/renderer.js +17 -39
  24. package/dist/engine/keyword-matcher/stopwords.d.ts +2 -2
  25. package/dist/engine/lifecycle-hints.d.ts +1 -1
  26. package/dist/engine/lifecycle-hints.js +13 -29
  27. package/dist/engine/provider-adapters/adapters/claude.d.ts +1 -1
  28. package/dist/engine/provider-adapters/adapters/gemini.d.ts +1 -1
  29. package/dist/engine/provider-adapters/adapters/gpt4.d.ts +1 -1
  30. package/dist/engine/provider-adapters/adapters/markdown.d.ts +1 -1
  31. package/dist/engine/safety/atomic-write-file.d.ts +1 -1
  32. package/dist/engine/safety/cross-process-lock.d.ts +1 -1
  33. package/dist/engine/safety/path-sanitizer.d.ts +1 -1
  34. package/dist/engine/security-analyzer/profile-detector.js +48 -16
  35. package/dist/engine/spec-format/copula-blacklist.d.ts +2 -2
  36. package/dist/engine/spec-format/lean-spec-generator.d.ts +2 -2
  37. package/dist/engine/spec-format/lean-spec-generator.js +9 -10
  38. package/dist/engine/spec-format/metadata-value-policy.d.ts +20 -20
  39. package/dist/engine/spec-generator/fallback-generator.js +13 -20
  40. package/dist/engine/spec-grounding/contract.d.ts +5 -0
  41. package/dist/engine/spec-grounding/contract.js +10 -6
  42. package/dist/engine/spec-migrator/lean-migration.js +9 -16
  43. package/dist/engine/text-signal-boundaries.js +45 -12
  44. package/dist/engine/universal-rules/rules/planu-modes.js +0 -2
  45. package/dist/engine/web-fetcher/stack-advisor.d.ts +2 -2
  46. package/dist/engine/web-fetcher/stack-advisor.js +78 -17
  47. package/dist/engine/well-known/manifest-schema.d.ts +33 -33
  48. package/dist/engine/workers/schema.d.ts +3 -3
  49. package/dist/hosts/claude-code/ux/mcp-resources.d.ts +4 -4
  50. package/dist/hosts/codex/rest-shim.d.ts +1 -1
  51. package/dist/native/lightweight-command-catalog.d.ts +30 -30
  52. package/dist/server/openapi-spec.d.ts +132 -132
  53. package/dist/tools/challenge-spec/challenge-report.d.ts +13 -0
  54. package/dist/tools/challenge-spec/challenge-report.js +55 -0
  55. package/dist/tools/challenge-spec/event-challenge-scenarios.js +8 -27
  56. package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.js +16 -32
  57. package/dist/tools/challenge-spec/resilience-challenge-scenarios-b.js +15 -20
  58. package/dist/tools/challenge-spec/scenario-collector.d.ts +13 -0
  59. package/dist/tools/challenge-spec/scenario-collector.js +86 -0
  60. package/dist/tools/challenge-spec/scenarios-data.js +10 -9
  61. package/dist/tools/challenge-spec/scenarios-failure.js +38 -54
  62. package/dist/tools/challenge-spec/scenarios-scale.js +14 -15
  63. package/dist/tools/challenge-spec/scenarios-security.d.ts +1 -1
  64. package/dist/tools/challenge-spec/scenarios-security.js +18 -11
  65. package/dist/tools/challenge-spec/scenarios-utils.d.ts +31 -5
  66. package/dist/tools/challenge-spec/scenarios-utils.js +102 -6
  67. package/dist/tools/challenge-spec/security-challenge-scenarios.d.ts +1 -1
  68. package/dist/tools/challenge-spec/security-challenge-scenarios.js +20 -25
  69. package/dist/tools/challenge-spec.js +46 -124
  70. package/dist/tools/clarify-requirements/multiple-choice.js +1 -1
  71. package/dist/tools/clarify-requirements/questions-context.js +11 -11
  72. package/dist/tools/clarify-requirements/questions.js +3 -23
  73. package/dist/tools/clarify-requirements.js +38 -0
  74. package/dist/tools/compliance-gate-handler.d.ts +1 -1
  75. package/dist/tools/create-spec/autopilot-analyzer.d.ts +1 -1
  76. package/dist/tools/create-spec/autopilot-analyzer.js +29 -54
  77. package/dist/tools/create-spec/post-creation.js +23 -4
  78. package/dist/tools/create-spec.js +51 -168
  79. package/dist/tools/delete-first/suggest-deletions.d.ts +1 -1
  80. package/dist/tools/elicit-requirements-handler.js +20 -12
  81. package/dist/tools/generate-batch-script.d.ts +2 -2
  82. package/dist/tools/init-project/rules-generator.js +0 -1
  83. package/dist/tools/oauth-handler.d.ts +1 -1
  84. package/dist/tools/register-sdd-tools.d.ts +1 -1
  85. package/dist/tools/register-spec-tools/core-spec-tools.js +11 -2
  86. package/dist/tools/render-spec-for-provider.d.ts +2 -2
  87. package/dist/tools/scan-orphan-spec-refs.d.ts +6 -6
  88. package/dist/tools/schemas/agents.d.ts +7 -7
  89. package/dist/tools/schemas/analysis.d.ts +11 -11
  90. package/dist/tools/schemas/delete.d.ts +5 -5
  91. package/dist/tools/schemas/github.d.ts +4 -4
  92. package/dist/tools/schemas/ide-config.d.ts +12 -12
  93. package/dist/tools/schemas/infra.d.ts +9 -9
  94. package/dist/tools/schemas/lifecycle.d.ts +50 -50
  95. package/dist/tools/schemas/llm-provider-schemas.d.ts +7 -7
  96. package/dist/tools/schemas/plugins-schemas.d.ts +12 -12
  97. package/dist/tools/schemas/project-graph.d.ts +6 -6
  98. package/dist/tools/schemas/rbac.d.ts +2 -2
  99. package/dist/tools/schemas/registry.d.ts +3 -3
  100. package/dist/tools/schemas/runtime-security.d.ts +2 -2
  101. package/dist/tools/schemas/session.d.ts +2 -2
  102. package/dist/tools/schemas/spec.d.ts +15 -15
  103. package/dist/tools/schemas/token-intelligence.d.ts +6 -6
  104. package/dist/tools/schemas/token-optimization.d.ts +2 -2
  105. package/dist/tools/schemas/workers-schema.d.ts +4 -4
  106. package/dist/tools/skill-bootstrap-handler.d.ts +7 -7
  107. package/dist/tools/suggest-stack.js +49 -13
  108. package/dist/tools/tool-registry/deprecated-stubs.d.ts +1 -1
  109. package/dist/tools/triage-request.d.ts +3 -3
  110. package/dist/tools/update-status/dod-gates.d.ts +8 -0
  111. package/dist/tools/update-status/dod-gates.js +68 -43
  112. package/dist/tools/update-status/file-sync.d.ts +1 -1
  113. package/dist/tools/update-status/file-sync.js +10 -45
  114. package/dist/tools/update-status/index.js +133 -97
  115. package/dist/tools/update-status/qa-gate.js +26 -0
  116. package/dist/tools/update-status/response-builder.d.ts +1 -1
  117. package/dist/tools/update-status/response-builder.js +11 -19
  118. package/dist/tools/update-status/transition-guard.d.ts +2 -2
  119. package/dist/tools/update-status/transition-guard.js +11 -8
  120. package/dist/tools/update-status-actions.d.ts +0 -1
  121. package/dist/tools/update-status-actions.js +10 -26
  122. package/dist/tools/update-status-convention-gate.d.ts +1 -6
  123. package/dist/tools/update-status-convention-gate.js +9 -33
  124. package/dist/tools/validate-lint.d.ts +1 -1
  125. package/dist/tools/validate-lint.js +39 -1
  126. package/dist/types/docs.d.ts +0 -4
  127. package/dist/types/env.d.ts +0 -4
  128. package/dist/types/estimation.d.ts +0 -2
  129. package/dist/types/multi-teammate-review.d.ts +4 -4
  130. package/dist/types/plugin-install.d.ts +7 -7
  131. package/dist/types/spec/core.d.ts +20 -0
  132. package/dist/types/spec/inputs.d.ts +1 -1
  133. package/dist/types/stack/index.d.ts +1 -1
  134. package/dist/types/stack/recommend.d.ts +10 -0
  135. package/package.json +21 -18
  136. package/planu-native.json +1 -1
  137. package/planu-plugin.json +1 -1
  138. package/src/i18n/messages/en.json +0 -72
  139. package/src/i18n/messages/es.json +0 -72
  140. package/src/i18n/messages/pt.json +0 -72
  141. package/dist/config/elicitation-dimensions.json +0 -51
  142. package/dist/engine/actuals-estimator.d.ts +0 -14
  143. package/dist/engine/actuals-estimator.js +0 -64
  144. package/dist/engine/cascade-hooks/hooks/living-specs.hook.d.ts +0 -3
  145. package/dist/engine/cascade-hooks/hooks/living-specs.hook.js +0 -19
  146. package/dist/engine/elicitation/non-interactive-defaults.d.ts +0 -9
  147. package/dist/engine/elicitation/non-interactive-defaults.js +0 -28
  148. package/dist/engine/elicitation/option-builder.d.ts +0 -9
  149. package/dist/engine/elicitation/option-builder.js +0 -85
  150. package/dist/engine/progress-writer.d.ts +0 -10
  151. package/dist/engine/progress-writer.js +0 -63
  152. package/dist/tools/clarify-requirements/interview-mode.d.ts +0 -42
  153. package/dist/tools/clarify-requirements/interview-mode.js +0 -342
  154. package/dist/tools/update-status/mode-hints.d.ts +0 -4
  155. package/dist/tools/update-status/mode-hints.js +0 -24
@@ -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
  };
@@ -7,20 +7,26 @@ import { adjustRecommendationRanking } from '../engine/self-improver.js';
7
7
  export async function handleSuggestStack(args) {
8
8
  const { projectType, requirements, constraints = [], projectId } = args;
9
9
  // If projectId provided, enrich with project knowledge
10
- let contextInfo = '';
10
+ const projectEvidence = [];
11
11
  if (projectId) {
12
12
  const knowledge = await knowledgeStore.getKnowledge(projectId);
13
13
  if (knowledge) {
14
- contextInfo =
15
- `\n\n**Project context:** ${knowledge.language}` +
16
- (knowledge.framework ? ` / ${knowledge.framework}` : '') +
17
- ` — architecture: ${knowledge.architecture.primary}` +
18
- ` — database: ${knowledge.database}`;
14
+ if (knowledge.language !== 'unknown') {
15
+ projectEvidence.push(`language=${knowledge.language}`);
16
+ }
17
+ if (knowledge.framework) {
18
+ projectEvidence.push(`framework=${knowledge.framework}`);
19
+ }
20
+ projectEvidence.push(`architecture=${knowledge.architecture.primary}`);
21
+ if (knowledge.database !== 'unknown') {
22
+ projectEvidence.push(`database=${knowledge.database}`);
23
+ }
19
24
  }
20
25
  }
21
26
  // SPEC-012: Load developer preferences to adjust recommendation ranking
22
27
  const developerPrefs = await globalStore.getDeveloperPreferences();
23
28
  const result = await suggestStack(projectType, requirements, constraints);
29
+ const grounding = result;
24
30
  // Apply developer preference rankings to ecosystem items
25
31
  const ecosystemItems = [...result.primary.ecosystem, ...result.alternative.ecosystem];
26
32
  const rankings = adjustRecommendationRanking(ecosystemItems, developerPrefs);
@@ -28,11 +34,14 @@ export async function handleSuggestStack(args) {
28
34
  const rejectedItems = [...rankings.entries()]
29
35
  .filter(([, score]) => score < -1)
30
36
  .map(([tool]) => tool);
31
- // Format primary recommendation
37
+ // Recommendations remain candidates until the user confirms a choice.
32
38
  const primaryLines = [
33
39
  `## ${t('tools.suggest_stack.description')}`,
34
40
  '',
35
- '### Primary Recommendation',
41
+ grounding.candidateNotice ??
42
+ 'Recommendations are optional candidates. This tool does not select or persist a stack.',
43
+ '',
44
+ '### Candidate A (unconfirmed)',
36
45
  '',
37
46
  `| Aspect | Value |`,
38
47
  `|--------|-------|`,
@@ -48,10 +57,10 @@ export async function handleSuggestStack(args) {
48
57
  '',
49
58
  `**Ecosystem:** ${result.primary.ecosystem.join(', ')}`,
50
59
  ].filter(Boolean);
51
- // Format alternative
60
+ // Format alternative candidate
52
61
  const altLines = [
53
62
  '',
54
- '### Alternative Recommendation',
63
+ '### Candidate B (unconfirmed)',
55
64
  '',
56
65
  `| Aspect | Value |`,
57
66
  `|--------|-------|`,
@@ -67,10 +76,10 @@ export async function handleSuggestStack(args) {
67
76
  '',
68
77
  `**Ecosystem:** ${result.alternative.ecosystem.join(', ')}`,
69
78
  ].filter(Boolean);
70
- // Why not others
79
+ // Other optional candidates
71
80
  const whyNotLines = [
72
81
  '',
73
- '### Why Not Others',
82
+ '### Other Candidates Considered',
74
83
  '',
75
84
  ...result.whyNotOthers.map((w) => `- **${w.name}:** ${w.reason}`),
76
85
  ];
@@ -86,18 +95,45 @@ export async function handleSuggestStack(args) {
86
95
  const prefsNote = rejectedItems.length > 0
87
96
  ? `\n\n> **Note:** Based on your preferences, these were previously rejected: ${rejectedItems.join(', ')}`
88
97
  : '';
98
+ const detectedEvidence = [
99
+ ...projectEvidence.map((item) => `project:${item}`),
100
+ ...(grounding.detectedEvidence ?? []),
101
+ ];
102
+ const evidenceLines = [
103
+ '',
104
+ '### Detected Evidence',
105
+ '',
106
+ ...(detectedEvidence.length > 0
107
+ ? detectedEvidence.map((item) => `- ${item}`)
108
+ : ['- No stack evidence was detected.']),
109
+ '',
110
+ '### User-Confirmed Choices',
111
+ '',
112
+ ...((grounding.confirmedChoices ?? []).length > 0
113
+ ? (grounding.confirmedChoices ?? []).map((choice) => `- ${choice.field}: ${choice.value}`)
114
+ : ['- None.']),
115
+ ];
89
116
  const text = [
90
117
  ...primaryLines,
91
118
  ...altLines,
92
119
  ...whyNotLines,
93
120
  ...mcpLines,
94
- contextInfo,
121
+ ...evidenceLines,
95
122
  prefsNote,
96
123
  '',
97
124
  `_Last checked: ${result.lastChecked}_`,
98
125
  ].join('\n');
99
126
  return {
100
127
  content: [{ type: 'text', text }],
128
+ structuredContent: {
129
+ selectionStatus: grounding.selectionStatus ?? 'unconfirmed',
130
+ detectedEvidence,
131
+ confirmedChoices: grounding.confirmedChoices ?? [],
132
+ candidates: [
133
+ { id: 'candidate-a', status: 'candidate', stack: result.primary },
134
+ { id: 'candidate-b', status: 'candidate', stack: result.alternative },
135
+ ],
136
+ },
101
137
  };
102
138
  }
103
139
  //# sourceMappingURL=suggest-stack.js.map
@@ -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>;
@@ -30,6 +30,13 @@ export type ValidateGateResult = {
30
30
  forcedReason: string;
31
31
  scoreSource: 'forced-best-effort-validateSpec';
32
32
  };
33
+ export type ValidationReportGateResult = {
34
+ ok: true;
35
+ score: number | null;
36
+ } | {
37
+ ok: false;
38
+ error: ToolResult;
39
+ };
33
40
  /**
34
41
  * SPEC-721 / SPEC-222 Trigger 1: Run validate engine before marking done.
35
42
  *
@@ -79,6 +86,7 @@ export interface DoneGateResult {
79
86
  * with reviewer evidence and all validation gates passing.
80
87
  */
81
88
  export declare function checkValidationReportGate(specId: string, projectId: string, force: boolean | undefined): Promise<ToolResult | null>;
89
+ export declare function readApprovedValidationReportGate(specId: string, projectId: string, force: boolean | undefined): Promise<ValidationReportGateResult>;
82
90
  /** SPEC-1051: Write spec-review evidence when a spec enters review. */
83
91
  export declare function writeSpecReviewArtifact(spec: Spec, specId: string, projectId: string): Promise<ToolResult | null>;
84
92
  /** SPEC-1051: Approval requires a dedicated spec reviewer artifact. */
@@ -280,8 +280,12 @@ async function recordForceDoneBypass(specId, projectId, reason, failingItems, wa
280
280
  * with reviewer evidence and all validation gates passing.
281
281
  */
282
282
  export async function checkValidationReportGate(specId, projectId, force) {
283
+ const result = await readApprovedValidationReportGate(specId, projectId, force);
284
+ return result.ok ? null : result.error;
285
+ }
286
+ export async function readApprovedValidationReportGate(specId, projectId, force) {
283
287
  if (force) {
284
- return null;
288
+ return { ok: true, score: null };
285
289
  }
286
290
  try {
287
291
  const { readArtifact } = await import('../../engine/handoff-artifacts/io.js');
@@ -289,60 +293,81 @@ export async function checkValidationReportGate(specId, projectId, force) {
289
293
  if (!result.ok) {
290
294
  const firstErr = result.errors[0];
291
295
  if (firstErr?.code === 'ARTIFACT_NOT_FOUND') {
292
- return validationReportGateError({
296
+ return {
297
+ ok: false,
298
+ error: validationReportGateError({
299
+ specId,
300
+ error: 'validation_report_missing',
301
+ message: 'No validation-report artifact exists for this spec. Run validate to generate implementation-review evidence before marking done.',
302
+ gates: [],
303
+ fixHint: 'Run validate for this spec, fix any failing gates, then retry update_status(done). Use force:true only with an audited reason.',
304
+ }),
305
+ };
306
+ }
307
+ return {
308
+ ok: false,
309
+ error: validationReportGateError({
293
310
  specId,
294
- error: 'validation_report_missing',
295
- message: 'No validation-report artifact exists for this spec. Run validate to generate implementation-review evidence before marking done.',
311
+ error: 'validation_report_invalid',
312
+ message: 'The validation-report artifact is malformed or uses an obsolete schema. Re-run validate so Planu can generate reviewer evidence.',
296
313
  gates: [],
297
- fixHint: 'Run validate for this spec, fix any failing gates, then retry update_status(done). Use force:true only with an audited reason.',
298
- });
299
- }
300
- return validationReportGateError({
301
- specId,
302
- error: 'validation_report_invalid',
303
- message: 'The validation-report artifact is malformed or uses an obsolete schema. Re-run validate so Planu can generate reviewer evidence.',
304
- gates: [],
305
- fixHint: 'Re-run validate for this spec. The report must include reviewer evidence and passing gates.',
306
- });
314
+ fixHint: 'Re-run validate for this spec. The report must include reviewer evidence and passing gates.',
315
+ }),
316
+ };
307
317
  }
308
318
  if (!result.payload.passed) {
309
- return validationReportGateError({
310
- specId,
311
- error: 'validation_report_failed',
312
- message: 'Validation report has passed:false — fix all failing gates before marking done.',
313
- gates: result.payload.gates,
314
- fixHint: 'Fix failing gates and re-run validate before marking done.',
315
- });
319
+ return {
320
+ ok: false,
321
+ error: validationReportGateError({
322
+ specId,
323
+ error: 'validation_report_failed',
324
+ message: 'Validation report has passed:false fix all failing gates before marking done.',
325
+ gates: result.payload.gates,
326
+ fixHint: 'Fix failing gates and re-run validate before marking done.',
327
+ }),
328
+ };
316
329
  }
317
330
  if (result.payload.minimalityReport?.blocked) {
318
- return validationReportGateError({
319
- specId,
320
- error: 'minimality_findings_block_done',
321
- message: 'Validation report contains blocking minimal implementation findings. Remove avoidable complexity or provide an audited forceStatusReason.',
322
- gates: result.payload.gates,
323
- fixHint: 'Fix blocking minimality findings and re-run validate before marking done. Use forceStatus only with an audited reason for accepted complexity.',
324
- });
331
+ return {
332
+ ok: false,
333
+ error: validationReportGateError({
334
+ specId,
335
+ error: 'minimality_findings_block_done',
336
+ message: 'Validation report contains blocking minimal implementation findings. Remove avoidable complexity or provide an audited forceStatusReason.',
337
+ gates: result.payload.gates,
338
+ fixHint: 'Fix blocking minimality findings and re-run validate before marking done. Use forceStatus only with an audited reason for accepted complexity.',
339
+ }),
340
+ };
325
341
  }
326
342
  if (result.payload.reviewer.verdict !== 'approved' ||
327
343
  result.payload.reviewer.agent.trim().length === 0) {
328
- return validationReportGateError({
329
- specId,
330
- error: 'validation_report_reviewer_not_approved',
331
- message: 'The implementation reviewer did not approve this spec. Fix the requested changes and re-run validate.',
332
- gates: result.payload.gates,
333
- fixHint: 'Fix reviewer findings and re-run validate before marking done.',
334
- });
344
+ return {
345
+ ok: false,
346
+ error: validationReportGateError({
347
+ specId,
348
+ error: 'validation_report_reviewer_not_approved',
349
+ message: 'The implementation reviewer did not approve this spec. Fix the requested changes and re-run validate.',
350
+ gates: result.payload.gates,
351
+ fixHint: 'Fix reviewer findings and re-run validate before marking done.',
352
+ }),
353
+ };
335
354
  }
336
- return null;
355
+ return {
356
+ ok: true,
357
+ score: typeof result.payload.score === 'number' ? result.payload.score : null,
358
+ };
337
359
  }
338
360
  catch (err) {
339
- return validationReportGateError({
340
- specId,
341
- error: 'validation_report_unreadable',
342
- message: `Could not read validation-report artifact: ${err instanceof Error ? err.message : String(err)}`,
343
- gates: [],
344
- fixHint: 'Re-run validate for this spec, then retry update_status(done).',
345
- });
361
+ return {
362
+ ok: false,
363
+ error: validationReportGateError({
364
+ specId,
365
+ error: 'validation_report_unreadable',
366
+ message: `Could not read validation-report artifact: ${err instanceof Error ? err.message : String(err)}`,
367
+ gates: [],
368
+ fixHint: 'Re-run validate for this spec, then retry update_status(done).',
369
+ }),
370
+ };
346
371
  }
347
372
  }
348
373
  /** SPEC-1051: Write spec-review evidence when a spec enters review. */
@@ -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;