@planu/cli 5.3.5 → 5.3.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 (57) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/config/runtime-policy.json +1 -0
  3. package/dist/engine/convention-scanner/codebase-scanner.js +1 -1
  4. package/dist/engine/hooks/handlers/on-impl-change.d.ts +17 -4
  5. package/dist/engine/hooks/handlers/on-impl-change.js +43 -44
  6. package/dist/engine/living-spec-analyzer.d.ts +5 -2
  7. package/dist/engine/living-spec-analyzer.js +5 -2
  8. package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
  9. package/dist/engine/planu-core.darwin-arm64.node.sbom.json +4 -4
  10. package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
  11. package/dist/engine/planu-core.darwin-x64.node.sbom.json +4 -4
  12. package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
  13. package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +4 -4
  14. package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
  15. package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +4 -4
  16. package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
  17. package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +4 -4
  18. package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
  19. package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +4 -4
  20. package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
  21. package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +4 -4
  22. package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
  23. package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +4 -4
  24. package/dist/engine/runtime-policy.js +7 -0
  25. package/dist/engine/scope-boundaries/contradiction-checker.js +28 -20
  26. package/dist/engine/spec-format/acceptance-criteria.js +5 -2
  27. package/dist/engine/spec-migrator/filesystem-import.js +113 -73
  28. package/dist/engine/spec-migrator/frontmatter-parser.js +15 -13
  29. package/dist/tools/auto-reconcile.js +1 -1
  30. package/dist/tools/challenge-spec/challenge-report.js +1 -1
  31. package/dist/tools/init-project/portable-index-reconciler.js +2 -1
  32. package/dist/tools/reconcile-spec.js +3 -1
  33. package/dist/tools/safe-handler.js +0 -2
  34. package/dist/tools/start-hooks/engine.d.ts +2 -3
  35. package/dist/tools/start-hooks/engine.js +50 -33
  36. package/dist/tools/start-hooks.d.ts +1 -1
  37. package/dist/tools/start-hooks.js +1 -2
  38. package/dist/tools/tool-registry/group-infra.js +36 -0
  39. package/dist/tools/update-status/transition-guard.js +17 -4
  40. package/dist/tools/validate-assurance.js +1 -1
  41. package/dist/tools/validate-lint.js +1 -1
  42. package/dist/tools/validate.d.ts +1 -0
  43. package/dist/tools/validate.js +21 -2
  44. package/dist/types/file-hooks.d.ts +4 -6
  45. package/dist/types/index.d.ts +0 -1
  46. package/dist/types/index.js +0 -1
  47. package/dist/types/mcp.d.ts +0 -19
  48. package/dist/types/runtime-policy.d.ts +1 -0
  49. package/dist/types/spec/core.d.ts +5 -0
  50. package/dist/types/spec/inputs.d.ts +16 -0
  51. package/package.json +9 -9
  52. package/planu-native.json +1 -1
  53. package/planu-plugin.json +1 -1
  54. package/dist/engine/hooks/agent-hook-migrator.d.ts +0 -9
  55. package/dist/engine/hooks/agent-hook-migrator.js +0 -140
  56. package/dist/types/hook-status-update.d.ts +0 -10
  57. package/dist/types/hook-status-update.js +0 -3
@@ -3,7 +3,6 @@
3
3
  // instead of crashing the server. Includes cancellation-aware execution deadlines.
4
4
  import { withUsageTracking } from './usage-tracking.js';
5
5
  import { sanitizePath } from './sanitize-path.js';
6
- import { ensureHooksStarted } from './start-hooks.js';
7
6
  import { ensureWorkersStarted } from '../engine/workers/index.js';
8
7
  import { recordError } from '../storage/error-telemetry-store.js';
9
8
  import { hashProjectPath } from '../storage/base-store.js';
@@ -158,7 +157,6 @@ function sanitizeArgs(args, toolName) {
158
157
  // planu_status is a read-only snapshot. Starting background services here made
159
158
  // its latency depend on repository size and violated the MCP readOnlyHint.
160
159
  if (toolName !== 'planu_status') {
161
- ensureHooksStarted(obj.projectPath);
162
160
  ensureWorkersStarted(obj.projectPath);
163
161
  }
164
162
  // Read-only check: track whether planu.json exists (no file writes)
@@ -1,13 +1,12 @@
1
1
  import type { ToolResult } from '../../types/index.js';
2
- import type { StartHooksInput, StopHooksInput } from '../../types/file-hooks.js';
2
+ import type { StartHooksInput, StopHooksInput, HookStatusInput } from '../../types/file-hooks.js';
3
3
  import { HookEngine } from '../../engine/hooks/hook-engine.js';
4
4
  import { FileWatcher } from '../../engine/hooks/file-watcher.js';
5
5
  import { ConfigLoader } from '../../engine/hooks/config-loader.js';
6
6
  export declare function getEngineInstance(): HookEngine | null;
7
7
  export declare function getWatcherInstance(): FileWatcher | null;
8
8
  export declare function getConfigLoaderInstance(): ConfigLoader | null;
9
- export declare function ensureHooksStarted(projectPath: string): void;
10
9
  export declare function handleStartHooks(args: StartHooksInput): Promise<ToolResult>;
11
10
  export declare function handleStopHooks(_args: StopHooksInput): Promise<ToolResult>;
12
- export declare function handleHookStatus(): Promise<ToolResult>;
11
+ export declare function handleHookStatus(args?: HookStatusInput): Promise<ToolResult>;
13
12
  //# sourceMappingURL=engine.d.ts.map
@@ -4,7 +4,7 @@ import { EventBus } from '../../engine/hooks/event-bus.js';
4
4
  import { FileWatcher } from '../../engine/hooks/file-watcher.js';
5
5
  import { ConfigLoader } from '../../engine/hooks/config-loader.js';
6
6
  import { Debouncer } from '../../engine/hooks/debouncer.js';
7
- import { handleOnImplChange } from '../../engine/hooks/handlers/on-impl-change.js';
7
+ import { handleOnImplChange, getLastImplChangeResult, } from '../../engine/hooks/handlers/on-impl-change.js';
8
8
  import { handleOnSpecChange } from '../../engine/hooks/handlers/on-spec-change.js';
9
9
  import { handleOnPushCheck, getLastPushCheckResult, } from '../../engine/hooks/handlers/on-push-check.js';
10
10
  import { handleOnSecurityCheck, isSecuritySensitiveFile, } from '../../engine/hooks/handlers/on-security-check.js';
@@ -13,7 +13,6 @@ import { listSpecs } from '../../storage/spec-store.js';
13
13
  import { hashProjectPath } from '../../storage/base-store.js';
14
14
  import { analyzeLivingSpec } from '../../engine/living-spec-analyzer.js';
15
15
  import { compactResult, formatKeyValue } from '../output-formatter.js';
16
- import { migrateAgentHooksIfNeeded } from '../../engine/hooks/agent-hook-migrator.js';
17
16
  // ---------------------------------------------------------------------------
18
17
  // Singleton state
19
18
  // ---------------------------------------------------------------------------
@@ -30,26 +29,27 @@ export function getWatcherInstance() {
30
29
  export function getConfigLoaderInstance() {
31
30
  return configLoaderInstance;
32
31
  }
33
- export function ensureHooksStarted(projectPath) {
34
- migrateAgentHooksIfNeeded(projectPath);
35
- if (engineInstance?.status().running) {
36
- return;
32
+ /**
33
+ * Resolve whether a hook type should run: it must be among the hooks the
34
+ * engine was explicitly started with, and the live ConfigLoader config must
35
+ * not disable it. Reads getConfig() live (not a start-time snapshot) so a
36
+ * hot config reload takes effect without restarting the engine (SPEC-1404).
37
+ * A hook type absent from config.hooks (e.g. on-impl-change in the defaults
38
+ * file) is treated as enabled, matching pre-existing behaviour.
39
+ */
40
+ function isHookEnabled(hookType, requestedHooks) {
41
+ if (!requestedHooks.includes(hookType)) {
42
+ return false;
37
43
  }
38
- void handleStartHooks({
39
- projectPath,
40
- hooks: [
41
- 'on-save',
42
- 'on-create',
43
- 'on-delete',
44
- 'on-commit',
45
- 'on-impl-change',
46
- 'on-spec-change',
47
- 'on-push-check',
48
- 'on-security-check',
49
- ],
50
- }).catch(() => {
51
- // Silently swallow — hooks are best-effort
52
- });
44
+ const config = configLoaderInstance?.getConfig();
45
+ /* v8 ignore next 3 -- defensive: unreachable while handleStartHooks owns configLoaderInstance's lifecycle */
46
+ if (!config) {
47
+ return false;
48
+ }
49
+ if (!config.enabled) {
50
+ return false;
51
+ }
52
+ return config.hooks[hookType]?.enabled !== false;
53
53
  }
54
54
  // ---------------------------------------------------------------------------
55
55
  // Start hooks
@@ -84,7 +84,7 @@ export async function handleStartHooks(args) {
84
84
  debouncerInstance = new Debouncer((event) => {
85
85
  const hookTypes = getApplicableHookTypes(event);
86
86
  for (const hookType of hookTypes) {
87
- if (enabledHooks.includes(hookType)) {
87
+ if (isHookEnabled(hookType, enabledHooks)) {
88
88
  void engineInstance?.dispatch(event, hookType);
89
89
  }
90
90
  }
@@ -100,7 +100,7 @@ export async function handleStartHooks(args) {
100
100
  configLoaderInstance.startWatching();
101
101
  engineInstance.start();
102
102
  watcherInstance.start();
103
- registerAdvancedHandlers(engineInstance, enabledHooks, projectPath, config);
103
+ registerAdvancedHandlers(engineInstance, enabledHooks, projectPath);
104
104
  const eventBusFromEngine = engineInstance.getEventBus();
105
105
  subscribeStatusChangeHandler(eventBusFromEngine,
106
106
  /* v8 ignore next 3 */ (_projectId) => Promise.resolve(true));
@@ -133,23 +133,40 @@ export async function handleStopHooks(_args) {
133
133
  // ---------------------------------------------------------------------------
134
134
  // Hook status
135
135
  // ---------------------------------------------------------------------------
136
- export async function handleHookStatus() {
136
+ export async function handleHookStatus(args = {}) {
137
137
  await Promise.resolve();
138
138
  if (!engineInstance) {
139
139
  return compactResult(formatKeyValue({ running: false, activeHooks: 0, pendingEvents: 0, eventHistory: 0 }, 'Hook Status'));
140
140
  }
141
141
  const status = engineInstance.status();
142
142
  const eventBus = engineInstance.getEventBus();
143
- return compactResult(formatKeyValue({
143
+ const summary = formatKeyValue({
144
144
  running: status.running,
145
145
  activeHooks: status.activeHooks.length,
146
146
  pendingEvents: status.pendingEvents,
147
147
  eventHistory: eventBus.getFullHistory().length,
148
- }, 'Hook Status'));
148
+ }, 'Hook Status');
149
+ const diagnostic = getLastImplChangeResult(args.projectPath);
150
+ if (!diagnostic) {
151
+ return compactResult(summary);
152
+ }
153
+ return compactResult(summary + '\n\n' + formatImplChangeDiagnostic(diagnostic));
149
154
  }
150
155
  // ---------------------------------------------------------------------------
151
156
  // Private helpers
152
157
  // ---------------------------------------------------------------------------
158
+ function formatImplChangeDiagnostic(diagnostic) {
159
+ const specLines = diagnostic.specsAnalyzed.map((s) => ` - ${s.specId}: ${String(s.completionPct)}%`);
160
+ return [
161
+ '### Last impl-change diagnostic',
162
+ `**changedFile**: ${diagnostic.changedFile}`,
163
+ `**anyAt100Percent**: ${String(diagnostic.autoReconciled)}`,
164
+ ...specLines,
165
+ '',
166
+ 'Process-local observation, never applied automatically. ' +
167
+ 'Apply it with reconcile_spec(livingSpec: true), or change lifecycle status with update_status.',
168
+ ].join('\n');
169
+ }
153
170
  function getApplicableHookTypes(event) {
154
171
  switch (event.type) {
155
172
  case 'modify':
@@ -178,7 +195,7 @@ function makeAnalyzeSpecFn() {
178
195
  status: minimal.status,
179
196
  progressPath: minimal.progressPath,
180
197
  };
181
- const report = await analyzeLivingSpec(specLike, projectPath);
198
+ const report = await analyzeLivingSpec(specLike, projectPath, { writeProgress: false });
182
199
  return {
183
200
  completionPct: report.completionPercent,
184
201
  progressUpdated: report.progressUpdated,
@@ -190,8 +207,8 @@ function makeAnalyzeSpecFn() {
190
207
  };
191
208
  };
192
209
  }
193
- function registerAdvancedHandlers(engine, enabledHooks, projectPath, config) {
194
- if (enabledHooks.includes('on-impl-change')) {
210
+ function registerAdvancedHandlers(engine, enabledHooks, projectPath) {
211
+ if (isHookEnabled('on-impl-change', enabledHooks)) {
195
212
  engine.register({
196
213
  name: 'on-impl-change',
197
214
  hookType: 'on-impl-change',
@@ -206,7 +223,7 @@ function registerAdvancedHandlers(engine, enabledHooks, projectPath, config) {
206
223
  catch {
207
224
  // Spec load failure — proceed without analysis
208
225
  }
209
- const analysisResult = await handleOnImplChange(event, projectPath, specs, makeAnalyzeSpecFn(), config.autoMarkDone !== false);
226
+ const analysisResult = await handleOnImplChange(event, projectPath, specs, makeAnalyzeSpecFn());
210
227
  const firstSpecId = analysisResult
211
228
  ? (analysisResult.specsAnalyzed[0]?.specId ?? null)
212
229
  : null;
@@ -220,7 +237,7 @@ function registerAdvancedHandlers(engine, enabledHooks, projectPath, config) {
220
237
  },
221
238
  });
222
239
  }
223
- if (enabledHooks.includes('on-spec-change')) {
240
+ if (isHookEnabled('on-spec-change', enabledHooks)) {
224
241
  engine.register({
225
242
  name: 'on-spec-change',
226
243
  hookType: 'on-spec-change',
@@ -237,7 +254,7 @@ function registerAdvancedHandlers(engine, enabledHooks, projectPath, config) {
237
254
  },
238
255
  });
239
256
  }
240
- if (enabledHooks.includes('on-push-check')) {
257
+ if (isHookEnabled('on-push-check', enabledHooks)) {
241
258
  engine.register({
242
259
  name: 'on-push-check',
243
260
  hookType: 'on-push-check',
@@ -265,7 +282,7 @@ function registerAdvancedHandlers(engine, enabledHooks, projectPath, config) {
265
282
  },
266
283
  });
267
284
  }
268
- if (enabledHooks.includes('on-security-check')) {
285
+ if (isHookEnabled('on-security-check', enabledHooks)) {
269
286
  engine.register({
270
287
  name: 'on-security-check',
271
288
  hookType: 'on-security-check',
@@ -1,3 +1,3 @@
1
- export { getEngineInstance, getWatcherInstance, getConfigLoaderInstance, ensureHooksStarted, handleStartHooks, handleStopHooks, handleHookStatus, } from './start-hooks/engine.js';
1
+ export { getEngineInstance, getWatcherInstance, getConfigLoaderInstance, handleStartHooks, handleStopHooks, handleHookStatus, } from './start-hooks/engine.js';
2
2
  export { handleConfigureHooks } from './start-hooks/configure.js';
3
3
  //# sourceMappingURL=start-hooks.d.ts.map
@@ -1,5 +1,4 @@
1
1
  // tools/start-hooks.ts — Barrel re-export. Implementation split into start-hooks/ subdirectory.
2
- // Existing imports (register-hooks-tools.ts, safe-handler.ts) continue to work unchanged.
3
- export { getEngineInstance, getWatcherInstance, getConfigLoaderInstance, ensureHooksStarted, handleStartHooks, handleStopHooks, handleHookStatus, } from './start-hooks/engine.js';
2
+ export { getEngineInstance, getWatcherInstance, getConfigLoaderInstance, handleStartHooks, handleStopHooks, handleHookStatus, } from './start-hooks/engine.js';
4
3
  export { handleConfigureHooks } from './start-hooks/configure.js';
5
4
  //# sourceMappingURL=start-hooks.js.map
@@ -28,6 +28,8 @@ import { handleValidateBrowser } from '../browser-validate-handler.js';
28
28
  import { WorkerStatusInputSchema, ConfigureWorkersInputSchema } from '../schemas/workers-schema.js';
29
29
  import { handleWorkerStatus } from '../worker-status-handler.js';
30
30
  import { handleConfigureWorkers } from '../configure-workers-handler.js';
31
+ // ── Hook tools (SPEC-1404: explicit start, no longer auto-started) ──────────
32
+ import { handleStartHooks, handleStopHooks, handleHookStatus } from '../start-hooks.js';
31
33
  // ── Plugin tools (register-plugin-tools.ts) ──────────────────────────────────
32
34
  import { handleManagePlugins } from '../manage-plugins-handler.js';
33
35
  import { handleScaffoldPlugin } from '../scaffold-plugin-handler.js';
@@ -434,6 +436,40 @@ export function registerInfraGroupTools(server) {
434
436
  },
435
437
  annotations: { title: 'Configure Workers', readOnlyHint: false },
436
438
  }, safeGoverned('configure_workers', (args) => handleConfigureWorkers(args)));
439
+ // ── Hook tools (SPEC-1404: explicit start, watcher never mutates spec state) ─
440
+ server.registerTool('start_hooks', {
441
+ description: 'Start the filesystem hook engine for a project: watches source and spec files and runs ' +
442
+ 'diagnostic-only analysis on change (never mutates spec lifecycle status). Must be called ' +
443
+ 'explicitly — hooks no longer start implicitly on other tool calls.',
444
+ inputSchema: {
445
+ projectPath: z.string().describe('Absolute path to the project root directory to watch'),
446
+ hooks: z
447
+ .array(z.string())
448
+ .optional()
449
+ .describe('Hook types to enable. Valid values: on-save, on-create, on-delete, on-test-pass, ' +
450
+ 'on-commit, on-impl-change, on-spec-change, on-push-check, on-security-check. ' +
451
+ 'Defaults to on-save, on-create, on-delete, on-impl-change, on-spec-change, on-security-check.'),
452
+ },
453
+ annotations: { title: 'Start Hooks', readOnlyHint: false },
454
+ }, safe(async (args) => handleStartHooks(args)));
455
+ server.registerTool('stop_hooks', {
456
+ description: 'Stop the running filesystem hook engine and release its watcher, debouncer, and config-loader resources.',
457
+ inputSchema: {
458
+ reason: z.string().optional().describe('Optional reason for stopping the hook engine'),
459
+ },
460
+ annotations: { title: 'Stop Hooks', readOnlyHint: false },
461
+ }, safe(async (args) => handleStopHooks(args)));
462
+ server.registerTool('hook_status', {
463
+ description: 'Show whether the filesystem hook engine is running, its active hooks and pending events, ' +
464
+ 'and the last diagnostic-only impl-change analysis (if any). Never applies changes.',
465
+ inputSchema: {
466
+ projectPath: z
467
+ .string()
468
+ .optional()
469
+ .describe('Absolute path to the project root, used to look up its diagnostic snapshot'),
470
+ },
471
+ annotations: { title: 'Hook Status', readOnlyHint: true },
472
+ }, safe(async (args) => handleHookStatus(args)));
437
473
  // ── Plugin tools ───────────────────────────────────────────────────────────
438
474
  server.registerTool('manage_plugins', {
439
475
  description: 'Manage Planu plugin lifecycle: search, list, activate, deactivate, uninstall, info. ' +
@@ -540,12 +540,25 @@ export function checkChallengeGate(spec, newStatus) {
540
540
  },
541
541
  };
542
542
  }
543
+ if (!Array.isArray(report.findings)) {
544
+ return {
545
+ content: [
546
+ {
547
+ type: 'text',
548
+ text: `Challenge gate blocked: the stored challenge report predates finding-level tracking, so its findings cannot be resolved. Run challenge_spec(specId="${spec.id}") to regenerate it before transitioning to review.`,
549
+ },
550
+ ],
551
+ isError: true,
552
+ structuredContent: {
553
+ error: 'CHALLENGE_GATE_BLOCKED',
554
+ code: 'STALE_CHALLENGE_REPORT',
555
+ fixHint: `Run challenge_spec(specId="${spec.id}") to regenerate the report with findings, then record resolution evidence.`,
556
+ },
557
+ };
558
+ }
543
559
  const resolutionEvidence = getResolvedChallengeEvidence(report);
544
560
  const addressedCount = resolutionEvidence.length;
545
- const findingsCount = Array.isArray(report.findings)
546
- ? report.findings.length
547
- : report.totalScenarios;
548
- const requiredCount = requiredResolvedChallenges(findingsCount);
561
+ const requiredCount = requiredResolvedChallenges(new Set(report.findings.map((finding) => finding.scenario)).size);
549
562
  if (addressedCount < requiredCount) {
550
563
  return {
551
564
  content: [
@@ -20,7 +20,7 @@ function readChangedSpecLogicFiles(projectPath) {
20
20
  'src/tools/create-spec',
21
21
  'src/tools/validate.ts',
22
22
  'src/engine',
23
- ], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
23
+ ], { encoding: 'utf-8', timeout: 5_000, stdio: ['ignore', 'pipe', 'ignore'] });
24
24
  changedFiles = output
25
25
  .split('\n')
26
26
  .map((line) => line.trim())
@@ -128,7 +128,7 @@ export async function runLintCheck(projectPath, lintCommand, context = {}) {
128
128
  source: 'local:planu-validate',
129
129
  });
130
130
  }
131
- return { passed: true, command, issueCount: 0, output };
131
+ return { passed: false, command, issueCount: 1, output };
132
132
  }
133
133
  const raw = commandOutput(err);
134
134
  const output = formatLintFailureDiagnostics(plan, raw);
@@ -4,6 +4,7 @@ import type { ValidateInput, ToolResult, ContractSpec, ContractValidation } from
4
4
  import type { ValidateWorkerProgress } from '../types/validate-worker.js';
5
5
  export { validateContractCompliance };
6
6
  export type { ContractSpec, ContractValidation };
7
+ export declare function createValidationStageRunner(onProgress?: (progress: ValidateWorkerProgress) => void): <T>(phase: string, command: string, work: () => T | Promise<T>) => Promise<T>;
7
8
  /** Public MCP boundary: persist and acknowledge validation work without running it inline. */
8
9
  export declare function handleValidate(args: ValidateInput): Promise<ToolResult>;
9
10
  export declare function executeValidate(args: ValidateInput, server?: McpServer, onProgress?: (progress: ValidateWorkerProgress) => void): Promise<ToolResult>;
@@ -85,7 +85,26 @@ import { resolveValidationWorktree, ValidationWorktreeError, } from '../engine/v
85
85
  // Re-export for external use (SPEC-018)
86
86
  export { validateContractCompliance };
87
87
  const VALIDATION_GATE_TOTAL = 8;
88
- function createValidationStageRunner(onProgress) {
88
+ const GATE_DEADLINE_EXEMPT_PHASES = new Set(['lint', 'compliance']);
89
+ async function runWithGateDeadline(phase, command, work) {
90
+ if (GATE_DEADLINE_EXEMPT_PHASES.has(phase)) {
91
+ return work();
92
+ }
93
+ const deadlineMs = getRuntimePolicy().validation.perGateTimeoutMs;
94
+ let timer;
95
+ const deadline = new Promise((_resolve, reject) => {
96
+ timer = setTimeout(() => {
97
+ reject(new Error(`[Planu] validate gate "${phase}" (${command}) exceeded its ${String(deadlineMs)}ms deadline`));
98
+ }, deadlineMs);
99
+ });
100
+ try {
101
+ return await Promise.race([Promise.resolve(work()), deadline]);
102
+ }
103
+ finally {
104
+ clearTimeout(timer);
105
+ }
106
+ }
107
+ export function createValidationStageRunner(onProgress) {
89
108
  const startedAt = Date.now();
90
109
  let completedGates = 0;
91
110
  return async (phase, command, work) => {
@@ -102,7 +121,7 @@ function createValidationStageRunner(onProgress) {
102
121
  };
103
122
  emit('running');
104
123
  try {
105
- const result = await work();
124
+ const result = await runWithGateDeadline(phase, command, work);
106
125
  completedGates += 1;
107
126
  emit('passed');
108
127
  return result;
@@ -26,12 +26,6 @@ export interface FileHookConfig {
26
26
  rateLimitPerFileMs: number;
27
27
  /** Per-hook-type configuration */
28
28
  hooks: Record<string, FileHookTypeConfig>;
29
- /**
30
- * When true (default), automatically mark a spec as 'done' when its
31
- * implementation analysis reaches 100% completion. Set to false to disable
32
- * the auto-done behaviour and only log the milestone.
33
- */
34
- autoMarkDone?: boolean;
35
29
  }
36
30
  export interface DebouncerOptions {
37
31
  /** Debounce window in milliseconds (default: 300) */
@@ -294,6 +288,10 @@ export interface StopHooksInput {
294
288
  /** Optional reason for stopping the hook engine */
295
289
  reason?: string;
296
290
  }
291
+ export interface HookStatusInput {
292
+ /** Absolute path to the project root, used to look up its diagnostic snapshot */
293
+ projectPath?: string;
294
+ }
297
295
  export interface ConfigureHooksInput {
298
296
  /** Absolute path to the project root directory */
299
297
  projectPath: string;
@@ -63,7 +63,6 @@ export * from './commercial-migration.js';
63
63
  export * from './coverage.js';
64
64
  export * from './ai-cost.js';
65
65
  export * from './hooks.js';
66
- export * from './hook-status-update.js';
67
66
  export * from './ci.js';
68
67
  export * from './spec-templates.js';
69
68
  export * from './spec-generator.js';
@@ -64,7 +64,6 @@ export * from './commercial-migration.js';
64
64
  export * from './coverage.js';
65
65
  export * from './ai-cost.js';
66
66
  export * from './hooks.js';
67
- export * from './hook-status-update.js';
68
67
  export * from './ci.js';
69
68
  export * from './spec-templates.js';
70
69
  export * from './spec-generator.js';
@@ -223,23 +223,4 @@ export interface ListSpecPromptsArgs {
223
223
  export interface SpecPromptRegistryEntry extends SpecPromptDefinition {
224
224
  specId: string;
225
225
  }
226
- export interface ClaudeHookEntry {
227
- type: string;
228
- prompt?: string;
229
- model?: string;
230
- statusMessage?: string;
231
- command?: string;
232
- once?: boolean;
233
- timeout?: number;
234
- [key: string]: unknown;
235
- }
236
- export interface ClaudeHookMatcher {
237
- matcher: string;
238
- hooks: ClaudeHookEntry[];
239
- }
240
- export type ClaudeHooksMap = Record<string, ClaudeHookMatcher[]>;
241
- export interface ClaudeSettings {
242
- hooks?: ClaudeHooksMap;
243
- [key: string]: unknown;
244
- }
245
226
  //# sourceMappingURL=mcp.d.ts.map
@@ -24,6 +24,7 @@ export interface RuntimePolicy {
24
24
  validation: {
25
25
  defaultDeadlineMs: number;
26
26
  durableLintTimeoutMs: number;
27
+ perGateTimeoutMs: number;
27
28
  complianceCommandTimeoutMs: number;
28
29
  noProgressTimeoutMs: number;
29
30
  submissionLockWaitMs: number;
@@ -266,6 +266,11 @@ export interface LivingSpecReport {
266
266
  analyzedAt: string;
267
267
  progressUpdated: boolean;
268
268
  }
269
+ /** Options for analyzeLivingSpec — controls whether ## Progress is written to spec.md. */
270
+ export interface AnalyzeLivingSpecOptions {
271
+ /** When true, write the analysis into spec.md's ## Progress section. Default: false. */
272
+ writeProgress?: boolean;
273
+ }
269
274
  export interface SpecVersion {
270
275
  specId: string;
271
276
  version: number;
@@ -1,5 +1,6 @@
1
1
  import type { SpecStatus, SpecType, SpecScope, SpecTarget, ToolResult } from '../common/index.js';
2
2
  import type { Actuals } from '../estimation.js';
3
+ import type { GlobalConfig } from '../project/config-metrics.js';
3
4
  import type { ProjectKnowledge } from '../project.js';
4
5
  import type { Spec, ChecklistCategory, SpecDiagram, SpecSplitSuggestion, ReconcileChange } from './core.js';
5
6
  export interface ReconciliationInvocationContext {
@@ -230,10 +231,25 @@ export interface FilesystemImportDeps {
230
231
  listSpecs: (projectId: string) => Promise<Spec[]>;
231
232
  createSpec: (projectId: string, spec: Spec) => Promise<Spec>;
232
233
  getSpecFresh?: (projectId: string, specId: string) => Promise<Spec | null>;
234
+ getGlobalConfig?: () => Promise<Partial<GlobalConfig>>;
233
235
  }
234
236
  export interface PortableIndexReconciliationResult extends FilesystemImportResult {
235
237
  repositoryFilesChanged: string[];
236
238
  }
239
+ export interface ImportEntryContext {
240
+ projectPath: string;
241
+ projectId: string;
242
+ deps: FilesystemImportDeps;
243
+ options: {
244
+ strictPortableContract?: boolean;
245
+ };
246
+ specsRoot: string;
247
+ globalConfig: Partial<GlobalConfig>;
248
+ storeSpecs: Spec[];
249
+ specsById: Map<string, Spec>;
250
+ importedSpecIds: string[];
251
+ failures: FilesystemImportFailure[];
252
+ }
237
253
  export type ChallengeSpecFocus = 'failures' | 'concurrency' | 'scale' | 'security' | 'data-consistency';
238
254
  export type { ScopeFilter, ScopeFilterConfig } from './core.js';
239
255
  export interface SplitAnalysisInput {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "5.3.5",
3
+ "version": "5.3.7",
4
4
  "description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,14 +35,14 @@
35
35
  "packageName": "@planu/core"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@planu/core-darwin-arm64": "5.3.5",
39
- "@planu/core-darwin-x64": "5.3.5",
40
- "@planu/core-linux-arm64-gnu": "5.3.5",
41
- "@planu/core-linux-arm64-musl": "5.3.5",
42
- "@planu/core-linux-x64-gnu": "5.3.5",
43
- "@planu/core-linux-x64-musl": "5.3.5",
44
- "@planu/core-win32-arm64-msvc": "5.3.5",
45
- "@planu/core-win32-x64-msvc": "5.3.5"
38
+ "@planu/core-darwin-arm64": "5.3.7",
39
+ "@planu/core-darwin-x64": "5.3.7",
40
+ "@planu/core-linux-arm64-gnu": "5.3.7",
41
+ "@planu/core-linux-arm64-musl": "5.3.7",
42
+ "@planu/core-linux-x64-gnu": "5.3.7",
43
+ "@planu/core-linux-x64-musl": "5.3.7",
44
+ "@planu/core-win32-arm64-msvc": "5.3.7",
45
+ "@planu/core-win32-x64-msvc": "5.3.7"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=24.0.0"
package/planu-native.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "5.3.5",
4
+ "version": "5.3.7",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
package/planu-plugin.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dev.planu.cli",
3
3
  "displayName": "Planu — Spec Driven Development",
4
4
  "description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
5
- "version": "5.3.5",
5
+ "version": "5.3.7",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": ["npx", "@planu/cli@latest"],
8
8
  "packageName": "@planu/cli",
@@ -1,9 +0,0 @@
1
- /**
2
- * Scan .claude/settings.json (project-level) and ~/.claude/settings.json (global)
3
- * for deprecated type:"agent" hooks and migrate them to type:"command" equivalents.
4
- *
5
- * Fire-and-forget safe: never throws, logs to stderr only.
6
- * Idempotent: each projectPath is only migrated once per process lifetime.
7
- */
8
- export declare function migrateAgentHooksIfNeeded(projectPath: string): void;
9
- //# sourceMappingURL=agent-hook-migrator.d.ts.map