@planu/cli 5.7.5 → 5.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +3 -3
  3. package/dist/.planu-build.json +1 -1
  4. package/dist/cli/commands/package-handoff.js +30 -5
  5. package/dist/config/compliance-profiles.json +36 -3
  6. package/dist/config/environment-schema.json +21 -0
  7. package/dist/config/hook-templates/planu-review-panel-trigger.sh +2 -3
  8. package/dist/config/hook-templates/planu-session-safeguard.sh +22 -15
  9. package/dist/config/official-sdd-tools.d.ts +1 -1
  10. package/dist/config/official-sdd-tools.js +11 -24
  11. package/dist/config/registries/hosts/codex.json +14 -30
  12. package/dist/config/registries/hosts/opencode.json +1 -6
  13. package/dist/config/server-instructions.js +0 -21
  14. package/dist/config/skill-templates/planu-context-assets.md +3 -6
  15. package/dist/config/skill-templates/planu-implement.md +2 -2
  16. package/dist/config/skill-templates/planu-multi-teammate-review.md +8 -12
  17. package/dist/config/skill-templates/planu-release.md +2 -7
  18. package/dist/config/skill-templates/planu-validate.md +3 -3
  19. package/dist/config/spec-templates/crud-rest-api/template.json +5 -1
  20. package/dist/config/spec-templates/file-upload-s3/template.json +5 -1
  21. package/dist/config/spec-templates/stripe-payments/template.json +5 -1
  22. package/dist/config/spec-templates/webhook-system/template.json +5 -1
  23. package/dist/config/subagent-templates/planu-challenger.md +4 -9
  24. package/dist/config/subagent-templates/planu-spec-implementer.md +2 -4
  25. package/dist/config/subagent-templates/planu-validator.md +5 -8
  26. package/dist/config/tool-groups.json +6 -144
  27. package/dist/engine/ambiguity-scorer.js +40 -1
  28. package/dist/engine/autopilot/bootstrap.js +1 -1
  29. package/dist/engine/code-impact-analyzer.js +2 -6
  30. package/dist/engine/context-orchestrator/index.js +2 -2
  31. package/dist/engine/convention-scanner/codebase-scanner.js +2 -2
  32. package/dist/engine/dogfooding/runtime-gap-detector.js +2 -2
  33. package/dist/engine/evidence-gates/artifact-reader.d.ts +1 -0
  34. package/dist/engine/evidence-gates/artifact-reader.js +1 -1
  35. package/dist/engine/evidence-gates/evidence-autofill.d.ts +13 -5
  36. package/dist/engine/evidence-gates/evidence-autofill.js +106 -43
  37. package/dist/engine/evidence-gates/evidence-skeletons.js +3 -1
  38. package/dist/engine/evidence-gates/lifecycle-gate.js +7 -4
  39. package/dist/engine/evidence-index/index-builder.js +21 -4
  40. package/dist/engine/execution/job-runtime.js +1 -1
  41. package/dist/engine/execution/package-handoff-job-executor.d.ts +22 -0
  42. package/dist/engine/execution/package-handoff-job-executor.js +490 -0
  43. package/dist/engine/execution/validate-job-executor.js +20 -8
  44. package/dist/engine/git/exec-git.d.ts +4 -0
  45. package/dist/engine/git/exec-git.js +14 -0
  46. package/dist/engine/handoff-artifacts/implementation-review-reader.js +15 -2
  47. package/dist/engine/handoff-artifacts/schemas.d.ts +2 -0
  48. package/dist/engine/handoff-artifacts/schemas.js +1 -0
  49. package/dist/engine/handoff-format.d.ts +36 -0
  50. package/dist/engine/handoff-format.js +314 -0
  51. package/dist/engine/handoff-packager.d.ts +5 -3
  52. package/dist/engine/handoff-packager.js +89 -32
  53. package/dist/engine/host-tool-filter.js +20 -49
  54. package/dist/engine/human-summary.js +14 -3
  55. package/dist/engine/implementation-contract/common.d.ts +8 -3
  56. package/dist/engine/implementation-contract/common.js +9 -6
  57. package/dist/engine/implementation-contract/evaluator.js +4 -6
  58. package/dist/engine/implementation-contract/renderer.js +18 -23
  59. package/dist/engine/project-health-checker.js +2 -2
  60. package/dist/engine/readiness-checker.js +43 -5
  61. package/dist/engine/reconcile/apply-changes.js +4 -0
  62. package/dist/engine/reconcile/propagate-mirrors.d.ts +3 -0
  63. package/dist/engine/reconcile/propagate-mirrors.js +311 -0
  64. package/dist/engine/self-healing/healer.js +12 -5
  65. package/dist/engine/self-healing/strategies/lint-fix.d.ts +1 -1
  66. package/dist/engine/self-healing/strategies/lint-fix.js +4 -1
  67. package/dist/engine/self-healing/strategies/test-fix.d.ts +4 -3
  68. package/dist/engine/self-healing/strategies/test-fix.js +32 -14
  69. package/dist/engine/self-healing/strategies/typescript-fix.d.ts +1 -1
  70. package/dist/engine/self-healing/strategies/typescript-fix.js +28 -10
  71. package/dist/engine/session/checkpoint-writer.js +8 -4
  72. package/dist/engine/session/session-tracker.d.ts +5 -16
  73. package/dist/engine/session/session-tracker.js +29 -26
  74. package/dist/engine/session-state/writer.js +3 -11
  75. package/dist/engine/skill-generator/workflow-skill-generator.d.ts +4 -2
  76. package/dist/engine/skill-generator/workflow-skill-generator.js +113 -38
  77. package/dist/engine/spec-format/bdd-parser.d.ts +1 -11
  78. package/dist/engine/spec-format/bdd-parser.js +40 -11
  79. package/dist/engine/spec-format/lean-spec-generator.js +32 -12
  80. package/dist/engine/spec-format/retired-scaffold.d.ts +3 -0
  81. package/dist/engine/spec-format/retired-scaffold.js +5 -0
  82. package/dist/engine/spec-format/technical-md-populator.d.ts +1 -0
  83. package/dist/engine/spec-format/technical-md-populator.js +27 -0
  84. package/dist/engine/spec-format/unified-spec-builder.d.ts +1 -0
  85. package/dist/engine/spec-format/unified-spec-builder.js +30 -20
  86. package/dist/engine/spec-grounding/contract.d.ts +3 -3
  87. package/dist/engine/spec-grounding/contract.js +65 -4
  88. package/dist/engine/spec-grounding/grounding-entry-parser.d.ts +5 -0
  89. package/dist/engine/spec-grounding/grounding-entry-parser.js +185 -0
  90. package/dist/engine/spec-quality/generic-output-gate.js +37 -3
  91. package/dist/engine/technical-enricher/index.js +4 -1
  92. package/dist/engine/type-safety-gate.js +3 -11
  93. package/dist/engine/universal-rules/catalog.js +0 -2
  94. package/dist/engine/universal-rules/rules/planu-workflow.js +1 -1
  95. package/dist/engine/validation/durable-validation.d.ts +3 -2
  96. package/dist/engine/validation/durable-validation.js +106 -40
  97. package/dist/engine/validation/validation-freshness.d.ts +3 -2
  98. package/dist/engine/validation/validation-freshness.js +22 -16
  99. package/dist/engine/validation/validation-worktree.js +34 -0
  100. package/dist/hosts/claude-code/ux/mcp-prompts.js +8 -8
  101. package/dist/index.js +3 -17
  102. package/dist/resources/process.js +38 -65
  103. package/dist/storage/session-state-store.js +2 -0
  104. package/dist/storage/spec-store.js +8 -1
  105. package/dist/tools/bump-spec-version.js +5 -5
  106. package/dist/tools/challenge-spec/scenarios-utils.js +2 -2
  107. package/dist/tools/configure-checkpoint-policy.js +3 -3
  108. package/dist/tools/create-spec-helpers.js +2 -5
  109. package/dist/tools/create-spec.js +21 -4
  110. package/dist/tools/facilitate.js +3 -6
  111. package/dist/tools/git/branch-ops.js +16 -4
  112. package/dist/tools/github-release-handler.js +1 -20
  113. package/dist/tools/init-project/agents-md-writer.js +6 -6
  114. package/dist/tools/init-project/claude-md-generator.d.ts +0 -1
  115. package/dist/tools/init-project/claude-md-generator.js +0 -35
  116. package/dist/tools/init-project/handler.js +7 -5
  117. package/dist/tools/init-project/per-client-files-writer.js +5 -5
  118. package/dist/tools/init-project/planu-workflow-generator.js +2 -13
  119. package/dist/tools/init-project/portable-index-reconciler.d.ts +2 -1
  120. package/dist/tools/init-project/portable-index-reconciler.js +7 -2
  121. package/dist/tools/init-project/rules-generator.d.ts +0 -11
  122. package/dist/tools/init-project/rules-generator.js +4 -85
  123. package/dist/tools/init-project/scaffold-writer.d.ts +0 -1
  124. package/dist/tools/init-project/scaffold-writer.js +1 -12
  125. package/dist/tools/init-project/skills-multi-teammate-review-writer.js +1 -1
  126. package/dist/tools/jobs/handlers.d.ts +0 -6
  127. package/dist/tools/jobs/handlers.js +13 -40
  128. package/dist/tools/list-specs.js +6 -21
  129. package/dist/tools/multi-teammate-review.js +1 -14
  130. package/dist/tools/package-handoff.d.ts +3 -2
  131. package/dist/tools/package-handoff.js +153 -427
  132. package/dist/tools/reconcile-session-safeguard-hook.js +22 -15
  133. package/dist/tools/register-platform-tools/design-stack-tools.js +3 -3
  134. package/dist/tools/register-sdd-tools.js +0 -23
  135. package/dist/tools/register-spec-tools/analysis-tools.js +22 -9
  136. package/dist/tools/register-spec-tools/core-spec-tools.js +115 -70
  137. package/dist/tools/schemas/index.d.ts +1 -1
  138. package/dist/tools/schemas/index.js +1 -1
  139. package/dist/tools/schemas/output-schemas.d.ts +1 -18
  140. package/dist/tools/schemas/output-schemas.js +1 -11
  141. package/dist/tools/schemas/package-handoff-output-schema.d.ts +40 -0
  142. package/dist/tools/schemas/package-handoff-output-schema.js +68 -0
  143. package/dist/tools/schemas/validate-output-schema.d.ts +1 -2
  144. package/dist/tools/schemas/validate-output-schema.js +3 -2
  145. package/dist/tools/session-checkpoint.js +2 -2
  146. package/dist/tools/skill-registry/index.d.ts +0 -1
  147. package/dist/tools/skill-registry/index.js +0 -1
  148. package/dist/tools/sync-spec-state-handler.d.ts +2 -2
  149. package/dist/tools/sync-spec-state-handler.js +23 -3
  150. package/dist/tools/tool-registry/core-tools.js +72 -164
  151. package/dist/tools/tool-registry/group-infra.js +2 -39
  152. package/dist/tools/tool-registry/group-quality-compliance.js +20 -15
  153. package/dist/tools/tool-registry-helpers.js +6 -2
  154. package/dist/tools/update-status/batch.js +17 -0
  155. package/dist/tools/update-status/dod-gates.js +14 -1
  156. package/dist/tools/update-status/done-receipt-verifier.d.ts +22 -0
  157. package/dist/tools/update-status/done-receipt-verifier.js +56 -26
  158. package/dist/tools/update-status/evidence-gate.js +10 -21
  159. package/dist/tools/update-status/file-sync.d.ts +1 -0
  160. package/dist/tools/update-status/file-sync.js +6 -0
  161. package/dist/tools/update-status/index.d.ts +6 -0
  162. package/dist/tools/update-status/index.js +39 -44
  163. package/dist/tools/update-status-actions.js +2 -7
  164. package/dist/tools/validate.js +34 -28
  165. package/dist/transports/oauth-validator.js +12 -1
  166. package/dist/transports/transport-factory.d.ts +2 -1
  167. package/dist/transports/transport-factory.js +19 -0
  168. package/dist/types/common/primitives.d.ts +2 -0
  169. package/dist/types/durable-job.d.ts +45 -0
  170. package/dist/types/durable-validation.d.ts +1 -1
  171. package/dist/types/evidence-autofill.d.ts +13 -1
  172. package/dist/types/handoff-artifacts.d.ts +2 -1
  173. package/dist/types/readiness.d.ts +8 -1
  174. package/dist/types/reconcile.d.ts +6 -0
  175. package/dist/types/skill-registry.d.ts +26 -1
  176. package/dist/types/spec/core.d.ts +5 -0
  177. package/dist/types/spec/inputs.d.ts +2 -2
  178. package/dist/types/spec-format.d.ts +11 -1
  179. package/dist/types/spec-grounding.d.ts +26 -0
  180. package/dist/types/transport.d.ts +1 -0
  181. package/dist/types/validation-receipt.d.ts +27 -0
  182. package/package.json +3 -2
  183. package/planu-plugin.json +13 -26
  184. package/src/i18n/messages/en.json +1 -1
  185. package/src/i18n/messages/es.json +1 -1
  186. package/src/i18n/messages/pt.json +1 -1
  187. package/dist/engine/skill-generator/conventions-hasher.d.ts +0 -7
  188. package/dist/engine/skill-generator/conventions-hasher.js +0 -24
  189. package/dist/engine/universal-rules/rules/agent-teams.d.ts +0 -3
  190. package/dist/engine/universal-rules/rules/agent-teams.js +0 -63
  191. package/dist/tools/clarify-requirements/multiple-choice.d.ts +0 -30
  192. package/dist/tools/clarify-requirements/multiple-choice.js +0 -306
  193. package/dist/tools/clarify-requirements/questions-context.d.ts +0 -9
  194. package/dist/tools/clarify-requirements/questions-context.js +0 -74
  195. package/dist/tools/clarify-requirements/questions.d.ts +0 -16
  196. package/dist/tools/clarify-requirements/questions.js +0 -96
  197. package/dist/tools/clarify-requirements.d.ts +0 -4
  198. package/dist/tools/clarify-requirements.js +0 -314
  199. package/dist/tools/code-graph-handler.d.ts +0 -6
  200. package/dist/tools/code-graph-handler.js +0 -72
  201. package/dist/tools/create-rule.d.ts +0 -4
  202. package/dist/tools/create-rule.js +0 -107
  203. package/dist/tools/feedback-handler.d.ts +0 -7
  204. package/dist/tools/feedback-handler.js +0 -150
  205. package/dist/tools/semantic-search-handler.d.ts +0 -7
  206. package/dist/tools/semantic-search-handler.js +0 -71
  207. package/dist/tools/skill-registry/search.d.ts +0 -11
  208. package/dist/tools/skill-registry/search.js +0 -159
  209. package/dist/tools/tool-registry/deprecated-stubs.d.ts +0 -19
  210. package/dist/tools/tool-registry/deprecated-stubs.js +0 -65
@@ -1,14 +1,17 @@
1
1
  // engine/self-healing/strategies/lint-fix.ts — SPEC-474: ESLint auto-fix strategy
2
2
  import { execFile } from 'node:child_process';
3
3
  import { promisify } from 'node:util';
4
+ import { qaCheckTimeoutMs } from '../../qa-gate.js';
4
5
  const execFileAsync = promisify(execFile);
5
6
  /**
6
7
  * Runs eslint --fix on the project and reports what was fixed.
7
8
  */
8
- export async function applyLintFix(projectPath, _details) {
9
+ export async function applyLintFix(projectPath, _details, timeoutMs = qaCheckTimeoutMs()) {
9
10
  try {
10
11
  await execFileAsync('pnpm', ['eslint', '--fix', 'src/', 'tests/', '--ext', '.ts'], {
11
12
  cwd: projectPath,
13
+ timeout: timeoutMs,
14
+ maxBuffer: 10 * 1024 * 1024,
12
15
  });
13
16
  return {
14
17
  fixed: true,
@@ -1,7 +1,8 @@
1
1
  import type { StrategyResult } from '../../../types/index.js';
2
2
  /**
3
- * Identifies failing tests from `pnpm test` output and reports what needs manual review.
4
- * Does NOT modify test files — test logic changes require human judgment.
3
+ * Identifies failing tests from the project's test command output and reports
4
+ * what needs manual review. Does NOT modify test files — test logic changes
5
+ * require human judgment.
5
6
  */
6
- export declare function applyTestFix(projectPath: string, _details: string): Promise<StrategyResult>;
7
+ export declare function applyTestFix(projectPath: string, _details: string, timeoutMs?: number): Promise<StrategyResult>;
7
8
  //# sourceMappingURL=test-fix.d.ts.map
@@ -1,5 +1,8 @@
1
1
  // engine/self-healing/strategies/test-fix.ts — SPEC-474: Test fix strategy
2
- import { execSync } from 'node:child_process';
2
+ import { detectPackageManager } from '../../project-dna/stack-detector.js';
3
+ import { runAbortableProcess } from '../../abortable-process-runner.js';
4
+ import { getRuntimePolicy } from '../../runtime-policy.js';
5
+ import { qaCheckTimeoutMs } from '../../qa-gate.js';
3
6
  /**
4
7
  * Parses failing test entries from vitest output.
5
8
  * Looks for:
@@ -42,25 +45,40 @@ function parseFailingTests(output) {
42
45
  }
43
46
  return results;
44
47
  }
48
+ function testCommandFor(packageManager) {
49
+ switch (packageManager) {
50
+ case 'yarn':
51
+ return { executable: 'yarn', args: ['test'] };
52
+ case 'npm':
53
+ return { executable: 'npm', args: ['test'] };
54
+ case 'bun':
55
+ return { executable: 'bun', args: ['run', 'test'] };
56
+ default:
57
+ return { executable: 'pnpm', args: ['test'] };
58
+ }
59
+ }
45
60
  /**
46
- * Identifies failing tests from `pnpm test` output and reports what needs manual review.
47
- * Does NOT modify test files — test logic changes require human judgment.
61
+ * Identifies failing tests from the project's test command output and reports
62
+ * what needs manual review. Does NOT modify test files — test logic changes
63
+ * require human judgment.
48
64
  */
49
- export function applyTestFix(projectPath, _details) {
50
- return Promise.resolve(runTestDiagnostic(projectPath));
65
+ export async function applyTestFix(projectPath, _details, timeoutMs = qaCheckTimeoutMs()) {
66
+ return runTestDiagnostic(await captureTestOutput(projectPath, timeoutMs));
51
67
  }
52
- function captureTestOutput(projectPath) {
53
- try {
54
- execSync('pnpm test', { cwd: projectPath, stdio: 'pipe', encoding: 'utf-8' });
68
+ async function captureTestOutput(projectPath, timeoutMs) {
69
+ const packageManager = await detectPackageManager(projectPath);
70
+ const { executable, args } = testCommandFor(packageManager);
71
+ const result = await runAbortableProcess(executable, args, {
72
+ cwd: projectPath,
73
+ timeoutMs,
74
+ maxBufferBytes: getRuntimePolicy().qualityAssurance.maximumOutputBufferBytes,
75
+ });
76
+ if (result.status === 0) {
55
77
  return null;
56
78
  }
57
- catch (err) {
58
- const errObj = err;
59
- return errObj.stdout ?? errObj.stderr ?? errObj.message ?? '';
60
- }
79
+ return [result.stdout, result.stderr].join('\n');
61
80
  }
62
- function runTestDiagnostic(projectPath) {
63
- const output = captureTestOutput(projectPath);
81
+ function runTestDiagnostic(output) {
64
82
  if (output === null) {
65
83
  return { fixed: true, reason: 'All tests pass — no failures detected', fixedIssues: [] };
66
84
  }
@@ -1,3 +1,3 @@
1
1
  import type { StrategyResult } from '../../../types/index.js';
2
- export declare function applyTypescriptFix(projectPath: string, _details: string): Promise<StrategyResult>;
2
+ export declare function applyTypescriptFix(projectPath: string, _details: string, timeoutMs?: number): Promise<StrategyResult>;
3
3
  //# sourceMappingURL=typescript-fix.d.ts.map
@@ -1,7 +1,10 @@
1
1
  // engine/self-healing/strategies/typescript-fix.ts — SPEC-474: TypeScript fix strategy
2
- import { execSync } from 'node:child_process';
3
2
  import { readFile, writeFile } from 'node:fs/promises';
4
3
  import { join } from 'node:path';
4
+ import { detectPackageManager } from '../../project-dna/stack-detector.js';
5
+ import { runAbortableProcess } from '../../abortable-process-runner.js';
6
+ import { getRuntimePolicy } from '../../runtime-policy.js';
7
+ import { qaCheckTimeoutMs } from '../../qa-gate.js';
5
8
  /**
6
9
  * Parses TS6133 (unused import) errors from tsc output.
7
10
  * Format: path/file.ts(10,5): error TS6133: 'foo' is declared but its value is never read.
@@ -94,22 +97,37 @@ async function fixFileUnusedImports(projectPath, filePath, entries) {
94
97
  }
95
98
  return { fixed: fixedDescriptions, skipped };
96
99
  }
100
+ function typecheckCommandFor(packageManager) {
101
+ switch (packageManager) {
102
+ case 'yarn':
103
+ return { executable: 'yarn', args: ['typecheck'] };
104
+ case 'npm':
105
+ return { executable: 'npm', args: ['run', 'typecheck'] };
106
+ case 'bun':
107
+ return { executable: 'bun', args: ['run', 'typecheck'] };
108
+ default:
109
+ return { executable: 'pnpm', args: ['typecheck'] };
110
+ }
111
+ }
97
112
  /**
98
113
  * Attempts to fix TypeScript errors by parsing tsc output and applying safe fixes:
99
114
  * - TS6133: removes unused imports when the pattern is unambiguous
100
115
  */
101
- function captureTypecheckOutput(projectPath) {
102
- try {
103
- execSync('pnpm typecheck', { cwd: projectPath, stdio: 'pipe', encoding: 'utf-8' });
116
+ async function captureTypecheckOutput(projectPath, timeoutMs) {
117
+ const packageManager = await detectPackageManager(projectPath);
118
+ const { executable, args } = typecheckCommandFor(packageManager);
119
+ const result = await runAbortableProcess(executable, args, {
120
+ cwd: projectPath,
121
+ timeoutMs,
122
+ maxBufferBytes: getRuntimePolicy().qualityAssurance.maximumOutputBufferBytes,
123
+ });
124
+ if (result.status === 0) {
104
125
  return null;
105
126
  }
106
- catch (err) {
107
- const errObj = err;
108
- return errObj.stdout ?? errObj.stderr ?? errObj.message ?? '';
109
- }
127
+ return [result.stdout, result.stderr].join('\n');
110
128
  }
111
- export async function applyTypescriptFix(projectPath, _details) {
112
- const output = captureTypecheckOutput(projectPath);
129
+ export async function applyTypescriptFix(projectPath, _details, timeoutMs = qaCheckTimeoutMs()) {
130
+ const output = await captureTypecheckOutput(projectPath, timeoutMs);
113
131
  if (output === null) {
114
132
  return { fixed: true, reason: 'No TypeScript errors found', fixedIssues: [] };
115
133
  }
@@ -3,7 +3,7 @@
3
3
  // Fire-and-forget safe — never throws to caller.
4
4
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
5
5
  import { join } from 'node:path';
6
- import { sessionTracker } from './session-tracker.js';
6
+ import { getSessionTracker } from './session-tracker.js';
7
7
  const MAX_DISPLAYED_CALLS = 5;
8
8
  const MAX_DISPLAYED_ERRORS = 3;
9
9
  const MAX_DISPLAYED_FILES = 10;
@@ -83,9 +83,13 @@ export async function writeCheckpoint(projectPath, snapshot) {
83
83
  * Call this from safe-handler.ts after each tool response.
84
84
  */
85
85
  export function maybeWriteCheckpoint(toolName, success, projectPath) {
86
- sessionTracker.recordCall(toolName, success);
87
- if (sessionTracker.shouldCheckpoint() && projectPath !== undefined) {
88
- void writeCheckpoint(projectPath, sessionTracker.getSnapshot()).catch(() => {
86
+ if (projectPath === undefined) {
87
+ return;
88
+ }
89
+ const tracker = getSessionTracker(projectPath);
90
+ tracker.recordCall(toolName, success);
91
+ if (tracker.shouldCheckpoint()) {
92
+ void writeCheckpoint(projectPath, tracker.getSnapshot()).catch(() => {
89
93
  /* resilient — never affects tool response */
90
94
  });
91
95
  }
@@ -1,31 +1,20 @@
1
1
  import type { AutoCheckpointPolicy, AutoSessionSnapshot } from '../../types/index.js';
2
- declare class SessionTracker {
2
+ export declare class SessionTracker {
3
3
  private callCount;
4
4
  private readonly lastCalls;
5
5
  private readonly modifiedFiles;
6
6
  private readonly errors;
7
- private policy;
8
- /** Record a tool call. Appends to lastCalls (capped at MAX_LAST_CALLS). */
9
7
  recordCall(tool: string, success: boolean): void;
10
- /** Record a file path as modified during this session. */
11
8
  recordFile(path: string): void;
12
- /** Record an error message (capped at MAX_ERRORS). */
13
9
  recordError(msg: string): void;
14
- /**
15
- * Returns true when a checkpoint should be written:
16
- * policy is enabled, callCount > 0, and callCount is a multiple of threshold.
17
- */
18
10
  shouldCheckpoint(): boolean;
19
- /** Return a compact snapshot of the current session state. */
20
11
  getSnapshot(): AutoSessionSnapshot;
21
- /** Update the checkpoint policy (partial update — unspecified fields keep current values). */
22
12
  setPolicy(p: Partial<AutoCheckpointPolicy>): void;
23
- /** Return the current checkpoint policy. */
24
13
  getPolicy(): AutoCheckpointPolicy;
25
- /** Reset the tracker state (useful for testing). */
26
14
  reset(): void;
27
15
  }
28
- /** Singleton session tracker one per process. */
29
- export declare const sessionTracker: SessionTracker;
30
- export {};
16
+ export declare function getSessionTracker(projectPath: string): SessionTracker;
17
+ export declare function setCheckpointPolicy(p: Partial<AutoCheckpointPolicy>): void;
18
+ export declare function getCheckpointPolicy(): AutoCheckpointPolicy;
19
+ export declare function resetSessionTrackers(): void;
31
20
  //# sourceMappingURL=session-tracker.d.ts.map
@@ -1,15 +1,12 @@
1
- // engine/session/session-tracker.ts — In-memory call counter and file modification tracker
2
- // for auto session checkpointing (SPEC-563).
3
1
  const DEFAULT_THRESHOLD = 10;
4
2
  const MAX_LAST_CALLS = 10;
5
3
  const MAX_ERRORS = 20;
6
- class SessionTracker {
4
+ let sharedPolicy = { threshold: DEFAULT_THRESHOLD, enabled: true };
5
+ export class SessionTracker {
7
6
  callCount = 0;
8
7
  lastCalls = [];
9
8
  modifiedFiles = new Set();
10
9
  errors = [];
11
- policy = { threshold: DEFAULT_THRESHOLD, enabled: true };
12
- /** Record a tool call. Appends to lastCalls (capped at MAX_LAST_CALLS). */
13
10
  recordCall(tool, success) {
14
11
  this.callCount++;
15
12
  this.lastCalls.push({ tool, success, timestamp: new Date().toISOString() });
@@ -17,25 +14,18 @@ class SessionTracker {
17
14
  this.lastCalls.shift();
18
15
  }
19
16
  }
20
- /** Record a file path as modified during this session. */
21
17
  recordFile(path) {
22
18
  this.modifiedFiles.add(path);
23
19
  }
24
- /** Record an error message (capped at MAX_ERRORS). */
25
20
  recordError(msg) {
26
21
  if (this.errors.length >= MAX_ERRORS) {
27
22
  this.errors.shift();
28
23
  }
29
24
  this.errors.push(msg);
30
25
  }
31
- /**
32
- * Returns true when a checkpoint should be written:
33
- * policy is enabled, callCount > 0, and callCount is a multiple of threshold.
34
- */
35
26
  shouldCheckpoint() {
36
- return (this.policy.enabled && this.callCount > 0 && this.callCount % this.policy.threshold === 0);
27
+ return (sharedPolicy.enabled && this.callCount > 0 && this.callCount % sharedPolicy.threshold === 0);
37
28
  }
38
- /** Return a compact snapshot of the current session state. */
39
29
  getSnapshot() {
40
30
  return {
41
31
  callCount: this.callCount,
@@ -44,28 +34,41 @@ class SessionTracker {
44
34
  errors: [...this.errors],
45
35
  };
46
36
  }
47
- /** Update the checkpoint policy (partial update — unspecified fields keep current values). */
48
37
  setPolicy(p) {
49
- if (p.threshold !== undefined) {
50
- this.policy = { ...this.policy, threshold: Math.max(1, Math.round(p.threshold)) };
51
- }
52
- if (p.enabled !== undefined) {
53
- this.policy = { ...this.policy, enabled: p.enabled };
54
- }
38
+ setCheckpointPolicy(p);
55
39
  }
56
- /** Return the current checkpoint policy. */
57
40
  getPolicy() {
58
- return { ...this.policy };
41
+ return getCheckpointPolicy();
59
42
  }
60
- /** Reset the tracker state (useful for testing). */
61
43
  reset() {
62
44
  this.callCount = 0;
63
45
  this.lastCalls.splice(0);
64
46
  this.modifiedFiles.clear();
65
47
  this.errors.splice(0);
66
- this.policy = { threshold: DEFAULT_THRESHOLD, enabled: true };
67
48
  }
68
49
  }
69
- /** Singleton session tracker — one per process. */
70
- export const sessionTracker = new SessionTracker();
50
+ const trackers = new Map();
51
+ export function getSessionTracker(projectPath) {
52
+ let tracker = trackers.get(projectPath);
53
+ if (!tracker) {
54
+ tracker = new SessionTracker();
55
+ trackers.set(projectPath, tracker);
56
+ }
57
+ return tracker;
58
+ }
59
+ export function setCheckpointPolicy(p) {
60
+ if (p.threshold !== undefined) {
61
+ sharedPolicy = { ...sharedPolicy, threshold: Math.max(1, Math.round(p.threshold)) };
62
+ }
63
+ if (p.enabled !== undefined) {
64
+ sharedPolicy = { ...sharedPolicy, enabled: p.enabled };
65
+ }
66
+ }
67
+ export function getCheckpointPolicy() {
68
+ return { ...sharedPolicy };
69
+ }
70
+ export function resetSessionTrackers() {
71
+ trackers.clear();
72
+ sharedPolicy = { threshold: DEFAULT_THRESHOLD, enabled: true };
73
+ }
71
74
  //# sourceMappingURL=session-tracker.js.map
@@ -2,8 +2,8 @@
2
2
  // engine/session-state/writer.ts — SPEC-633: Write session.json universal state file
3
3
  import { readFile, mkdir } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
- import { execSync } from 'node:child_process';
6
5
  import { stableJsonStringify, stableWriteGeneratedFile, stripRootUpdatedAt, } from '../generated-artifact-writer.js';
6
+ import { execGitCapture } from '../git/exec-git.js';
7
7
  // eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1699
8
8
  import { projectRuntimeDir } from '../../storage/storage-layout.js';
9
9
  const ROOT_SESSION = 'session.json';
@@ -18,11 +18,7 @@ export function rootFile(projectPath) {
18
18
  }
19
19
  function getModifiedFiles(projectPath) {
20
20
  try {
21
- const out = execSync('git diff --name-only HEAD 2>/dev/null', {
22
- cwd: projectPath,
23
- encoding: 'utf-8',
24
- stdio: ['pipe', 'pipe', 'pipe'],
25
- });
21
+ const out = execGitCapture('git diff --name-only HEAD 2>/dev/null', projectPath);
26
22
  return out.trim().split('\n').filter(Boolean).slice(0, 10);
27
23
  }
28
24
  catch {
@@ -31,11 +27,7 @@ function getModifiedFiles(projectPath) {
31
27
  }
32
28
  function getBranch(projectPath) {
33
29
  try {
34
- const out = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', {
35
- cwd: projectPath,
36
- encoding: 'utf-8',
37
- stdio: ['pipe', 'pipe', 'pipe'],
38
- });
30
+ const out = execGitCapture('git rev-parse --abbrev-ref HEAD 2>/dev/null', projectPath);
39
31
  return out.trim() || null;
40
32
  }
41
33
  catch {
@@ -3,8 +3,10 @@ import type { WorkflowSkillGenerationOptions, WorkflowSkillGenerationResult } fr
3
3
  export type { WorkflowSkillGenerationOptions, WorkflowSkillGenerationResult, } from '../../types/skill-registry.js';
4
4
  /**
5
5
  * Generate all 22 workflow skill files for the given project.
6
- * Respects idempotency: only rewrites if conventions changed.
7
- * Writes registry entries via writeSkillRegistryEntry (SPEC-680).
6
+ * Refreshes a pristine copy when Planu's shipped body changes; never touches
7
+ * a user-authored or user-edited file (SPEC-1457).
8
+ * Writes registry entries via writeSkillRegistryEntry (SPEC-680) only when a
9
+ * file is actually rewritten.
8
10
  */
9
11
  export declare function generateWorkflowSkills(projectPath: string, knowledge: ProjectKnowledge, options?: WorkflowSkillGenerationOptions): Promise<WorkflowSkillGenerationResult>;
10
12
  //# sourceMappingURL=workflow-skill-generator.d.ts.map
@@ -1,28 +1,35 @@
1
1
  import { technologyValue } from '../technology-registry.js';
2
- // src/engine/skill-generator/workflow-skill-generator.ts — SPEC-685
2
+ // src/engine/skill-generator/workflow-skill-generator.ts — SPEC-685, refreshed by SPEC-1457
3
3
  // Generates all 22 workflow skill files for a project.
4
4
  // Delegates placeholder substitution to adaptSkillContent (SPEC-682 — DRY).
5
5
  // Registers each skill via writeSkillRegistryEntry (SPEC-680).
6
6
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
7
+ import { createHash } from 'node:crypto';
7
8
  import { join } from 'node:path';
8
9
  import { adaptSkillContent } from '../skill-adapter/index.js';
9
10
  // eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
10
11
  import { writeSkillRegistryEntry } from '../../storage/skill-registry-storage.js';
11
12
  import { WORKFLOW_SKILL_DEFINITIONS } from './skill-definitions.js';
12
- import { hashConventions } from './conventions-hasher.js';
13
13
  // ---------------------------------------------------------------------------
14
14
  // Constants
15
15
  // ---------------------------------------------------------------------------
16
16
  const CLAUDE_SKILLS_REL = '.claude/skills';
17
- const HASH_MARKER_PREFIX = '<!-- planu-conventions-hash: ';
17
+ const HASH_MARKER_PREFIX = '<!-- planu-skill-hash: ';
18
18
  const HASH_MARKER_SUFFIX = ' -->';
19
+ const LEGACY_HASH_MARKER_PREFIX = '<!-- planu-conventions-hash: ';
20
+ const MARKER_SCAN_WINDOW = 5;
21
+ const HEX_HASH_PATTERN = /^[0-9a-f]{64}$/;
22
+ const LEGACY_HEX_HASH_PATTERN = /^[0-9a-f]{16}$/;
19
23
  // ---------------------------------------------------------------------------
20
24
  // Helpers
21
25
  // ---------------------------------------------------------------------------
22
- /** Build the full skill markdown with hash marker on line 2. */
26
+ /** SHA-256 hex digest of the exact bytes of the rendered skill content. */
27
+ function digestContent(content) {
28
+ return createHash('sha256').update(content, 'utf8').digest('hex');
29
+ }
30
+ /** Build the full skill markdown with the content-bound hash marker on line 2. */
23
31
  function buildSkillContent(adaptedBody, hash) {
24
32
  const marker = `${HASH_MARKER_PREFIX}${hash}${HASH_MARKER_SUFFIX}`;
25
- // Insert hash marker as second line (after the first line of the body)
26
33
  const lines = adaptedBody.split('\n');
27
34
  if (lines.length < 2) {
28
35
  return adaptedBody + '\n' + marker + '\n';
@@ -30,16 +37,58 @@ function buildSkillContent(adaptedBody, hash) {
30
37
  lines.splice(1, 0, marker);
31
38
  return lines.join('\n');
32
39
  }
33
- /** Extract the hash from line 2 of an existing skill file. Returns null if absent or malformed. */
34
- function extractExistingHash(content) {
40
+ /** Rendered content with the marker line at `lineIndex` removed, for the pristine digest check. */
41
+ function contentWithoutMarkerLine(content, lineIndex) {
42
+ const lines = content.split('\n');
43
+ lines.splice(lineIndex, 1);
44
+ return lines.join('\n');
45
+ }
46
+ function extractMarkerValue(line, prefix) {
47
+ if (!line.startsWith(prefix) || !line.endsWith(HASH_MARKER_SUFFIX)) {
48
+ return null;
49
+ }
50
+ return line.slice(prefix.length, -HASH_MARKER_SUFFIX.length);
51
+ }
52
+ /**
53
+ * Fail-closed marker reader: any duplicate marker, mixed marker formats, or a
54
+ * malformed hash value is reported as 'invalid' so the caller treats the file
55
+ * as user-modified rather than risk overwriting an edit.
56
+ */
57
+ function readMarker(content) {
35
58
  const lines = content.split('\n');
36
- for (let i = 0; i < Math.min(lines.length, 5); i++) {
37
- const line = lines[i] ?? '';
38
- if (line.startsWith(HASH_MARKER_PREFIX) && line.endsWith(HASH_MARKER_SUFFIX)) {
39
- return line.slice(HASH_MARKER_PREFIX.length, -HASH_MARKER_SUFFIX.length);
59
+ const window = lines.slice(0, MARKER_SCAN_WINDOW);
60
+ let currentHash = '';
61
+ let currentLineIndex = -1;
62
+ let currentCount = 0;
63
+ let legacyHash = '';
64
+ let legacyCount = 0;
65
+ for (let lineIndex = 0; lineIndex < window.length; lineIndex++) {
66
+ const line = window[lineIndex] ?? '';
67
+ const currentValue = extractMarkerValue(line, HASH_MARKER_PREFIX);
68
+ if (currentValue !== null) {
69
+ currentCount += 1;
70
+ currentHash = currentValue;
71
+ currentLineIndex = lineIndex;
72
+ continue;
73
+ }
74
+ const legacyValue = extractMarkerValue(line, LEGACY_HASH_MARKER_PREFIX);
75
+ if (legacyValue !== null) {
76
+ legacyCount += 1;
77
+ legacyHash = legacyValue;
40
78
  }
41
79
  }
42
- return null;
80
+ if (currentCount > 1 || legacyCount > 1 || (currentCount === 1 && legacyCount === 1)) {
81
+ return { kind: 'invalid' };
82
+ }
83
+ if (currentCount === 1) {
84
+ return HEX_HASH_PATTERN.test(currentHash)
85
+ ? { kind: 'current', hash: currentHash, lineIndex: currentLineIndex }
86
+ : { kind: 'invalid' };
87
+ }
88
+ if (legacyCount === 1) {
89
+ return LEGACY_HEX_HASH_PATTERN.test(legacyHash) ? { kind: 'legacy' } : { kind: 'invalid' };
90
+ }
91
+ return { kind: 'absent' };
43
92
  }
44
93
  /** Apply fallback values when knowledge fields are null. */
45
94
  function applyFallbacks(knowledge) {
@@ -53,55 +102,81 @@ function applyFallbacks(knowledge) {
53
102
  language: knowledge.language || technologyValue('technology-typescript-969545'),
54
103
  };
55
104
  }
105
+ async function decideForFile(destPath, newDigest, force) {
106
+ let existingContent;
107
+ try {
108
+ existingContent = await readFile(destPath, 'utf-8');
109
+ }
110
+ catch (err) {
111
+ const code = err.code;
112
+ if (code === 'ENOENT') {
113
+ return { action: 'write' };
114
+ }
115
+ return { action: 'skip-error', message: err instanceof Error ? err.message : String(err) };
116
+ }
117
+ const marker = readMarker(existingContent);
118
+ switch (marker.kind) {
119
+ case 'invalid':
120
+ case 'absent':
121
+ return { action: 'skip-user-modified' };
122
+ case 'legacy':
123
+ return { action: 'write' };
124
+ case 'current': {
125
+ const onDiskDigest = digestContent(contentWithoutMarkerLine(existingContent, marker.lineIndex));
126
+ if (onDiskDigest !== marker.hash) {
127
+ return { action: 'skip-user-modified' };
128
+ }
129
+ if (force || marker.hash !== newDigest) {
130
+ return { action: 'write' };
131
+ }
132
+ return { action: 'skip-unchanged' };
133
+ }
134
+ }
135
+ }
56
136
  // ---------------------------------------------------------------------------
57
137
  // Main export
58
138
  // ---------------------------------------------------------------------------
59
139
  /**
60
140
  * Generate all 22 workflow skill files for the given project.
61
- * Respects idempotency: only rewrites if conventions changed.
62
- * Writes registry entries via writeSkillRegistryEntry (SPEC-680).
141
+ * Refreshes a pristine copy when Planu's shipped body changes; never touches
142
+ * a user-authored or user-edited file (SPEC-1457).
143
+ * Writes registry entries via writeSkillRegistryEntry (SPEC-680) only when a
144
+ * file is actually rewritten.
63
145
  */
64
146
  export async function generateWorkflowSkills(projectPath, knowledge, options = {}) {
65
147
  const targets = options.targets ?? ['claude'];
66
148
  const force = options.force ?? false;
67
149
  // Apply fallbacks so placeholders always resolve
68
150
  const resolvedKnowledge = applyFallbacks(knowledge);
69
- // Compute current conventions hash (same for all skills in this run)
70
- const currentHash = hashConventions(resolvedKnowledge);
71
151
  // Ensure output directories exist
72
152
  const claudeSkillsDir = join(projectPath, CLAUDE_SKILLS_REL);
73
153
  await mkdir(claudeSkillsDir, { recursive: true });
74
154
  const written = [];
75
155
  const skipped = [];
156
+ const userModified = [];
76
157
  const errors = {};
77
158
  for (const def of WORKFLOW_SKILL_DEFINITIONS) {
78
159
  try {
79
160
  const skillFileName = `${def.name}.md`;
80
161
  const destPath = join(claudeSkillsDir, skillFileName);
81
- // --- Idempotency check ---
82
- if (!force) {
83
- let existingContent = null;
84
- try {
85
- existingContent = await readFile(destPath, 'utf-8');
86
- }
87
- catch {
88
- // File does not exist — will write
89
- }
90
- if (existingContent !== null) {
91
- const existingHash = extractExistingHash(existingContent);
92
- // Scenario 9: missing hash marker → treat as foreign, overwrite
93
- // Scenario 3: same hash → skip
94
- if (existingHash !== null && existingHash === currentHash) {
95
- skipped.push(def.name);
96
- continue;
97
- }
98
- // existingHash === null (missing marker) → fall through and overwrite
99
- }
100
- }
101
162
  // --- Adapt content via SPEC-682 (DRY — no duplicated placeholder logic) ---
102
163
  const { adapted } = adaptSkillContent(def.templateBody, resolvedKnowledge);
164
+ const newDigest = digestContent(adapted);
165
+ const decision = await decideForFile(destPath, newDigest, force);
166
+ if (decision.action === 'skip-unchanged') {
167
+ skipped.push(def.name);
168
+ continue;
169
+ }
170
+ if (decision.action === 'skip-user-modified') {
171
+ userModified.push(def.name);
172
+ continue;
173
+ }
174
+ if (decision.action === 'skip-error') {
175
+ errors[def.name] = decision.message;
176
+ continue;
177
+ }
103
178
  // --- Build final content with hash marker ---
104
- const finalContent = buildSkillContent(adapted, currentHash);
179
+ const finalContent = buildSkillContent(adapted, newDigest);
105
180
  // --- Write .claude/skills/{name}.md ---
106
181
  if (targets.includes('claude')) {
107
182
  await writeFile(destPath, finalContent, 'utf-8');
@@ -121,6 +196,6 @@ export async function generateWorkflowSkills(projectPath, knowledge, options = {
121
196
  errors[def.name] = err instanceof Error ? err.message : String(err);
122
197
  }
123
198
  }
124
- return { written, skipped, errors };
199
+ return { written, skipped, userModified, errors };
125
200
  }
126
201
  //# sourceMappingURL=workflow-skill-generator.js.map
@@ -1,16 +1,6 @@
1
1
  import type { BddScenario, BddStep, LeanCriterion } from '../../types/index.js';
2
2
  export type { BddScenario, BddStep };
3
- /**
4
- * Parses Gherkin-style blocks from a markdown string.
5
- *
6
- * Recognises:
7
- * Scenario: <title>
8
- * Given ...
9
- * When ...
10
- * Then ...
11
- * And ...
12
- * But ...
13
- */
3
+ export declare function extractInlineTestPaths(text: string): string[];
14
4
  export declare function parseBddScenarios(markdown: string): BddScenario[];
15
5
  /**
16
6
  * Converts an array of BddScenario objects into YAML lines for the frontmatter.