@planu/cli 3.9.14 → 4.0.0

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 (71) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/cli/commands/spec.js +20 -1
  3. package/dist/cli/commands/status.js +18 -1
  4. package/dist/config/license-plans.json +1 -0
  5. package/dist/engine/ai-integration/codex/hooks-generator.js +1 -0
  6. package/dist/engine/ai-integration/gemini/settings-generator.js +4 -1
  7. package/dist/engine/ai-integration/kiro/hooks-generator.js +2 -1
  8. package/dist/engine/autopilot/action-registry.js +5 -14
  9. package/dist/engine/autopilot/state-updater.js +13 -10
  10. package/dist/engine/cascade-hooks/hooks/git-auto-stage.hook.js +3 -0
  11. package/dist/engine/cascade-hooks/hooks/html-regen.hook.js +1 -1
  12. package/dist/engine/cascade-hooks/hooks/status-json.hook.js +1 -1
  13. package/dist/engine/cascade-hooks/state-drift-detector.d.ts +1 -1
  14. package/dist/engine/cascade-hooks/state-drift-detector.js +15 -12
  15. package/dist/engine/git/planu-autocommit.d.ts +1 -0
  16. package/dist/engine/git/planu-autocommit.js +6 -0
  17. package/dist/engine/git-hook-injector.js +3 -3
  18. package/dist/engine/handoff-artifacts/io.js +3 -2
  19. package/dist/engine/handoff-packager.js +2 -1
  20. package/dist/engine/hooks/full-spectrum-generator.d.ts +2 -1
  21. package/dist/engine/hooks/full-spectrum-generator.js +5 -3
  22. package/dist/engine/release/postmortem-generator.d.ts +1 -1
  23. package/dist/engine/release/postmortem-generator.js +3 -2
  24. package/dist/engine/safety/cross-process-lock.js +2 -2
  25. package/dist/engine/session/checkpoint-writer.js +0 -1
  26. package/dist/engine/session-context-generator.js +4 -1
  27. package/dist/engine/spec-audit/index.js +2 -2
  28. package/dist/engine/spec-audit/report-writer.d.ts +1 -1
  29. package/dist/engine/spec-audit/report-writer.js +5 -4
  30. package/dist/engine/spec-migrator/index.d.ts +1 -0
  31. package/dist/engine/spec-migrator/index.js +1 -0
  32. package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +9 -0
  33. package/dist/engine/spec-migrator/planu-canonical-policy.js +62 -0
  34. package/dist/engine/spec-migrator/planu-root-cleaner.js +18 -94
  35. package/dist/engine/spec-migrator/strict-planu-cleanup.d.ts +6 -0
  36. package/dist/engine/spec-migrator/strict-planu-cleanup.js +199 -0
  37. package/dist/engine/spec-summary-html.d.ts +5 -5
  38. package/dist/engine/spec-summary-html.js +7 -32
  39. package/dist/storage/gaps-log.js +4 -4
  40. package/dist/storage/transition-log.js +3 -2
  41. package/dist/tools/audit-specs-drift.js +3 -3
  42. package/dist/tools/create-spec/post-creation.d.ts +2 -1
  43. package/dist/tools/create-spec/post-creation.js +9 -11
  44. package/dist/tools/create-spec/spec-builder.js +1 -1
  45. package/dist/tools/create-spec.js +42 -18
  46. package/dist/tools/flag-spec-gap.d.ts +1 -1
  47. package/dist/tools/flag-spec-gap.js +1 -1
  48. package/dist/tools/generate-dashboard.js +3 -3
  49. package/dist/tools/housekeeping-sweep.js +16 -0
  50. package/dist/tools/init-project/git-setup.js +11 -2
  51. package/dist/tools/init-project/handler.js +1 -27
  52. package/dist/tools/init-project/migration-runner.js +8 -0
  53. package/dist/tools/license-gate.d.ts +1 -0
  54. package/dist/tools/license-gate.js +5 -1
  55. package/dist/tools/list-specs.js +13 -0
  56. package/dist/tools/register-sdd-tools.d.ts +1 -1
  57. package/dist/tools/register-sdd-tools.js +1 -0
  58. package/dist/tools/register-spec-tools/core-spec-tools.js +16 -0
  59. package/dist/tools/spec-lock-handler.js +1 -1
  60. package/dist/tools/tool-registry/group-misc.js +4 -4
  61. package/dist/tools/update-status/batch.d.ts +3 -0
  62. package/dist/tools/update-status/batch.js +96 -0
  63. package/dist/tools/update-status/dod-gates.js +1 -1
  64. package/dist/tools/update-status/file-sync.js +3 -1
  65. package/dist/tools/update-status/index.js +15 -2
  66. package/dist/tools/update-status-actions.js +2 -6
  67. package/dist/tools/validate.js +27 -0
  68. package/dist/tools/workspace-dashboard-handler.js +6 -9
  69. package/dist/types/git.d.ts +1 -1
  70. package/dist/types/spec-format.d.ts +26 -0
  71. package/package.json +1 -1
@@ -2,14 +2,14 @@ import { checkGate } from '../engine/clarification-gate/gate.js';
2
2
  import { upsertToken, hashQuestions } from '../engine/clarification-gate/token-store.js';
3
3
  import { ti } from '../i18n/index.js';
4
4
  import { knowledgeStore, specStore } from '../storage/index.js';
5
- import { formatSuccess, addNextSteps, toolResult } from './response-helpers.js';
5
+ import { formatSuccess, addNextSteps, toolResult, interactiveResult } from './response-helpers.js';
6
6
  import { writeFile, mkdir, rm, readFile, stat } from 'node:fs/promises';
7
7
  import { createHash } from 'node:crypto';
8
8
  import { estimateSpec } from '../engine/estimator.js';
9
9
  import { checkSpecReadiness } from '../engine/readiness-checker.js';
10
10
  import { buildSpecContext, buildSplitResult } from './create-spec/spec-builder.js';
11
11
  import { validateConstitution } from './create-spec/constitution-validator.js';
12
- import { setupGitBranch, checkContradictions, fireSpecCreatedHook, generatePostCreationSuggestions, runAutopilotAsync, } from './create-spec/post-creation.js';
12
+ import { setupGitBranch, checkContradictions, fireSpecCreatedHook, generatePostCreationSuggestions, runAutopilotAsync, getAsyncAnalysisPath, } from './create-spec/post-creation.js';
13
13
  import { notifyStoreChange } from '../engine/doc-generator/portal/regen-hook.js';
14
14
  import { compactObj } from '../engine/compact-obj.js';
15
15
  import { buildCreateSpecSummary } from '../engine/human-summary.js';
@@ -36,6 +36,7 @@ import { runPriorDecisionsHint } from './create-spec/adapters/prior-decisions-hi
36
36
  import { suggestOutOfScope } from '../engine/scope-boundaries/index.js';
37
37
  import { adviseSimilarSpecs } from '../engine/complexity-budget/index.js';
38
38
  import { issuePlannerToken } from '../engine/reviewer-tokens/issuer.js';
39
+ import { generateInteractiveQuestions } from './create-spec/question-generator.js';
39
40
  /** SPEC-584: Persist a clarification token when interactive questions are emitted. Best-effort. */
40
41
  async function persistClarificationToken(earlyReturn, projectId, toolName) {
41
42
  try {
@@ -65,7 +66,7 @@ async function runClarificationGate(projectPath, toolName, answers) {
65
66
  });
66
67
  return gateResult.allow ? null : gateResult.error;
67
68
  }
68
- function handleClarification(_server, _description, _knowledge, autopilot, params) {
69
+ function handleClarification(_server, description, knowledge, autopilot, params) {
69
70
  if (!autopilot.needsClarification) {
70
71
  return null;
71
72
  }
@@ -75,12 +76,32 @@ function handleClarification(_server, _description, _knowledge, autopilot, param
75
76
  if (hasAnswers) {
76
77
  return null;
77
78
  }
78
- // The description is too vague — ask the LLM to gather more context from the user.
79
- // No hardcoded question templates: the host LLM asks its own contextual questions.
79
+ const generatedQuestions = generateInteractiveQuestions(description, knowledge);
80
+ const questions = generatedQuestions.length > 0
81
+ ? generatedQuestions
82
+ : [
83
+ {
84
+ question: 'What specific behavior should this spec deliver?',
85
+ header: 'Scope',
86
+ options: [
87
+ {
88
+ label: 'User workflow (Recommended)',
89
+ description: 'Focus the spec on the end-to-end user behavior and completion state.',
90
+ },
91
+ {
92
+ label: 'Backend behavior',
93
+ description: 'Focus the spec on API, persistence, jobs, or integration behavior.',
94
+ },
95
+ {
96
+ label: 'Cleanup/fix',
97
+ description: 'Focus the spec on removing stale behavior or fixing a known bug.',
98
+ },
99
+ ],
100
+ multiSelect: false,
101
+ },
102
+ ];
80
103
  return {
81
- earlyReturn: toolResult('The description needs more detail before a spec can be created.\n\n' +
82
- 'Ask the user to clarify: what specific behavior they want, who will use it, ' +
83
- 'and what "done" looks like. Then call create_spec again with the enriched description.', { needsClarification: true }),
104
+ earlyReturn: interactiveResult(questions, 'The description needs more detail before a spec can be created. Answer the questions, then retry create_spec with clarificationAnswers or an enriched description.', 'universal'),
84
105
  };
85
106
  }
86
107
  const HIGH_RISK_WARNING = 'High-risk spec — consider: ' +
@@ -236,15 +257,12 @@ function computeIdempotencyKey(title, projectPath) {
236
257
  }
237
258
  /**
238
259
  * SPEC-781: Check if async analysis has completed for a given spec.
239
- * Returns { pending: true } when .analysis.json is absent (analysis still running or never started).
260
+ * Returns { pending: true } when external analysis state is absent.
240
261
  */
241
262
  async function checkAnalysisStatus(projectPath, specId) {
242
263
  try {
243
- const { glob } = await import('glob');
244
- const { join: pathJoin } = await import('node:path');
245
- const pattern = pathJoin(projectPath, 'planu/specs', `${specId}-*`, '.analysis.json');
246
- const matches = await glob(pattern);
247
- return { pending: matches.length === 0 };
264
+ await readFile(getAsyncAnalysisPath(projectPath, specId), 'utf-8');
265
+ return { pending: false };
248
266
  }
249
267
  catch {
250
268
  return { pending: true };
@@ -358,6 +376,12 @@ export async function handleCreateSpec(inputParams, server) {
358
376
  return {
359
377
  content: [{ type: 'text', text: DESCRIPTION_EXCEED_MSG }],
360
378
  isError: true,
379
+ structuredContent: {
380
+ error: 'DESCRIPTION_TOO_LONG',
381
+ maxChars: DESCRIPTION_MAX_CHARS,
382
+ actualChars: inputParams.description.length,
383
+ fixHint: 'Create with a summary description under 3000 chars, then append detailed context through reconcile_spec.',
384
+ },
361
385
  };
362
386
  }
363
387
  // SPEC-509: resolve projectPath from git root when omitted
@@ -406,7 +430,7 @@ export async function handleCreateSpec(inputParams, server) {
406
430
  const idempotencyKey = computeIdempotencyKey(inputParams.title, resolvedPath);
407
431
  const existingByKey = await findByIdempotencyKey(resolvedPath, idempotencyKey, hashProjectPath(resolvedPath));
408
432
  if (existingByKey) {
409
- // SPEC-781: Check if async analysis has completed (.analysis.json exists)
433
+ // SPEC-781: Check if async analysis has completed in external project data.
410
434
  const analysisStatus = await checkAnalysisStatus(resolvedPath, existingByKey.id);
411
435
  return {
412
436
  content: [
@@ -630,7 +654,7 @@ export async function handleCreateSpec(inputParams, server) {
630
654
  });
631
655
  // Build result (SPEC-461: lean — no progress, HTML, diagrams, scope filters)
632
656
  const result = {
633
- // SPEC-781: async analysis running in background (writes .analysis.json when done)
657
+ // SPEC-781: async analysis running in background (external project data)
634
658
  pendingAnalysis: true,
635
659
  specId: spec.id,
636
660
  title: spec.title,
@@ -684,7 +708,7 @@ export async function handleCreateSpec(inputParams, server) {
684
708
  }
685
709
  // Dispatch hook event (fire-and-forget)
686
710
  fireSpecCreatedHook(projectId, spec, params.projectPath ?? '');
687
- // SPEC-781: Fire-and-forget async analysis (writes .analysis.json when complete)
711
+ // SPEC-781: Fire-and-forget async analysis (external project data)
688
712
  runAutopilotAsync(spec.id, params.projectPath ?? '', description);
689
713
  // SPEC-713: Track warnings from budgeted post-creation steps
690
714
  const budgetWarnings = [];
@@ -770,7 +794,7 @@ export async function handleCreateSpec(inputParams, server) {
770
794
  if (budgetWarnings.length > 0) {
771
795
  result.budgetWarnings = budgetWarnings;
772
796
  }
773
- // SPEC-464: Removed auto-regeneration of planu/index.html use generate_dashboard on demand
797
+ // SPEC-1017: No auto-regeneration of project-tree dashboard HTML.
774
798
  if (params.projectPath) {
775
799
  notifyStoreChange(params.projectPath, 'specs');
776
800
  }
@@ -13,7 +13,7 @@ export interface FlagSpecGapInput {
13
13
  * Handle the flag_spec_gap tool invocation.
14
14
  *
15
15
  * Validates inputs (severity enum, affectedSpecs pattern + existence),
16
- * applies defaults, and appends a hash-chained entry to planu/research/gaps.jsonl.
16
+ * applies defaults, and appends a hash-chained entry to external Planu project data.
17
17
  */
18
18
  export declare function handleFlagSpecGap(input: FlagSpecGapInput): Promise<ToolResult>;
19
19
  //# sourceMappingURL=flag-spec-gap.d.ts.map
@@ -8,7 +8,7 @@ const VALID_SEVERITIES = ['low', 'medium', 'high'];
8
8
  * Handle the flag_spec_gap tool invocation.
9
9
  *
10
10
  * Validates inputs (severity enum, affectedSpecs pattern + existence),
11
- * applies defaults, and appends a hash-chained entry to planu/research/gaps.jsonl.
11
+ * applies defaults, and appends a hash-chained entry to external Planu project data.
12
12
  */
13
13
  export async function handleFlagSpecGap(input) {
14
14
  const { projectId, specId, description } = input;
@@ -4,11 +4,11 @@ import { specStore } from '../storage/index.js';
4
4
  import { hashProjectPath } from '../storage/base-store.js';
5
5
  export function registerGenerateDashboardTools(server) {
6
6
  server.registerTool('generate_spec_dashboard', {
7
- description: 'Regenerate planu/index.html dashboard with all specs, metrics, and pagination. Call after bulk changes or to fix a stale dashboard.',
7
+ description: 'Deprecated compatibility tool. Planu no longer writes generated dashboard HTML into planu/.',
8
8
  inputSchema: {
9
9
  projectPath: z.string().describe('Absolute path to the project root directory'),
10
10
  },
11
- annotations: { readOnlyHint: false },
11
+ annotations: { readOnlyHint: true },
12
12
  }, async (args) => {
13
13
  const projectId = hashProjectPath(args.projectPath);
14
14
  const specs = await specStore.listSpecs(projectId);
@@ -18,7 +18,7 @@ export function registerGenerateDashboardTools(server) {
18
18
  content: [
19
19
  {
20
20
  type: 'text',
21
- text: `Dashboard regenerated: ${String(specs.length)} specs (${String(doneCount)} done). File: planu/index.html`,
21
+ text: `Dashboard generation is deprecated; no project files were written. Current specs: ${String(specs.length)} (${String(doneCount)} done).`,
22
22
  },
23
23
  ],
24
24
  };
@@ -143,6 +143,21 @@ export async function handleHousekeepingSweep(args, resolvedProjectPath) {
143
143
  catch {
144
144
  /* best-effort — never block housekeeping_sweep */
145
145
  }
146
+ let strictPlanuCleanup = null;
147
+ try {
148
+ const { runStrictPlanuCleanup, validateStrictPlanuLayout } = await import('../engine/spec-migrator/index.js');
149
+ if (!dryRun) {
150
+ const cleanup = await runStrictPlanuCleanup(projectPath);
151
+ strictPlanuCleanup = { deleted: cleanup.deleted, merged: cleanup.merged, offenders: [] };
152
+ }
153
+ else {
154
+ const validation = await validateStrictPlanuLayout(projectPath);
155
+ strictPlanuCleanup = { deleted: [], merged: [], offenders: validation.offenders };
156
+ }
157
+ }
158
+ catch {
159
+ /* best-effort — never block housekeeping_sweep */
160
+ }
146
161
  // SPEC-1012: Detect and auto-fix specs released on main but still in non-terminal status
147
162
  let releaseStatusDrift = null;
148
163
  try {
@@ -186,6 +201,7 @@ export async function handleHousekeepingSweep(args, resolvedProjectPath) {
186
201
  details: report,
187
202
  ...(orphanSummary ? { orphanBrainstorming: orphanSummary } : {}),
188
203
  ...(ephemeralResult ? { ephemeralArtifacts: ephemeralResult } : {}),
204
+ ...(strictPlanuCleanup ? { strictPlanuCleanup } : {}),
189
205
  ...(releaseStatusDrift ? { releaseStatusDrift } : {}),
190
206
  },
191
207
  };
@@ -121,9 +121,18 @@ async function configureGitignore(projectPath) {
121
121
  // SPEC-724: Gitignore heal_spec_docs backup files (.bak.<ts>)
122
122
  const planuIgnores = [
123
123
  'planu/*.html',
124
- 'planu/*.pdf',
125
124
  'planu/status.json',
126
- 'planu/session-state.json',
125
+ 'planu/CHANGELOG.md',
126
+ 'planu/.housekeeping-history.jsonl',
127
+ 'planu/audits/',
128
+ 'planu/handoffs/',
129
+ 'planu/data/',
130
+ 'planu/state/',
131
+ 'planu/.locks/',
132
+ 'planu/specs/data/',
133
+ 'planu/specs/**/.analysis.json',
134
+ 'planu/specs/**/technical-report.html',
135
+ 'planu/specs/**/reference/',
127
136
  'planu/specs/**/*.bak.*',
128
137
  ];
129
138
  for (const entry of planuIgnores) {
@@ -24,7 +24,7 @@ import { readAutoInstallFlag, orchestrateSkillInstalls, runHealthCheckWithBaseli
24
24
  import { injectProactiveRules } from '../../engine/claude-md-injector/index.js';
25
25
  import { discoverSkillsForInit } from '../../engine/skill-bootstrap/registry-fetcher.js';
26
26
  import { join } from 'node:path';
27
- import { stat, readFile } from 'node:fs/promises';
27
+ import { stat } from 'node:fs/promises';
28
28
  import { generateAgentTeamsRulesIfMissing, generateWorkflowRulesIfMissing, } from './rules-generator.js';
29
29
  import { installUniversalRules } from '../../engine/universal-rules/installer.js';
30
30
  import { detectHost } from '../../engine/host-detection/detect-host.js';
@@ -126,32 +126,6 @@ export async function handleInitProject(params, server) {
126
126
  // Read-only — we surface the finding but never touch existing data.
127
127
  const ancestorReport = await detectAncestorDataDirs(projectPath, projectId);
128
128
  const ancestorWarning = buildAncestorDataWarning(ancestorReport);
129
- // SPEC-540: Idempotency check — if planu/status.json exists, return early with existing projectId
130
- const statusJsonPath = join(projectPath, 'planu', 'status.json');
131
- try {
132
- const statusRaw = await readFile(statusJsonPath, 'utf-8');
133
- const statusData = JSON.parse(statusRaw);
134
- const existingProjectId = typeof statusData.projectId === 'string' ? statusData.projectId : null;
135
- if (existingProjectId) {
136
- const baseText = `Project already initialized at ${projectPath}. Using existing projectId: ${existingProjectId}. Use list_specs to see existing specs or create_spec to add new ones.`;
137
- const text = ancestorWarning !== null ? `${baseText}\n\n⚠️ ${ancestorWarning}` : baseText;
138
- return {
139
- content: [{ type: 'text', text }],
140
- structuredContent: {
141
- projectId: existingProjectId,
142
- projectPath,
143
- alreadyInitialized: true,
144
- message: `Project already initialized at ${projectPath}. Using existing projectId: ${existingProjectId}.`,
145
- ...(ancestorWarning !== null
146
- ? { ancestorDataWarning: ancestorWarning, ancestorDataDirs: ancestorReport }
147
- : {}),
148
- },
149
- };
150
- }
151
- }
152
- catch {
153
- // status.json does not exist or is not parseable — proceed with normal init
154
- }
155
129
  if (ancestorWarning !== null) {
156
130
  console.warn(`[Planu] init_project: ${ancestorWarning}`);
157
131
  }
@@ -63,6 +63,14 @@ export async function runSpecMigrations(projectPath, projectId, knowledge) {
63
63
  catch {
64
64
  /* best-effort — never block init_project */
65
65
  }
66
+ // SPEC-1017: strict managed planu/ cleanup after all legacy migrations.
67
+ try {
68
+ const { runStrictPlanuCleanup } = await import('../../engine/spec-migrator/index.js');
69
+ await runStrictPlanuCleanup(projectPath);
70
+ }
71
+ catch {
72
+ /* best-effort — never block init_project */
73
+ }
66
74
  return { discoveryResult, migrationResult, folderMigrationResult };
67
75
  }
68
76
  /** Return all specs for the project (needed by caller for health check refs). */
@@ -7,6 +7,7 @@ export declare function invalidateTierCache(): void;
7
7
  * Returns 'enterprise' for owner token, 'pro' if a license key env var is set, 'free' otherwise.
8
8
  */
9
9
  export declare function getTierSync(): PricingTier;
10
+ export declare function shouldBypassDailyRateLimit(toolName: string): boolean;
10
11
  export declare function withLicenseGate<T>(toolName: string, handler: (args: T, extra?: unknown) => Promise<ToolResult>): (args: T, extra?: unknown) => Promise<ToolResult>;
11
12
  /**
12
13
  * Wraps a handler with rate limiting (free tier only) and usage event recording.
@@ -88,6 +88,10 @@ export function getTierSync() {
88
88
  // ---------------------------------------------------------------------------
89
89
  const sessionTierCache = new Map();
90
90
  const SESSION_TIER_TTL_MS = 10 * 60 * 1000; // 10 min — longer since online validation is expensive
91
+ const RATE_LIMIT_EXEMPT_TOOLS = new Set(['planu_status', 'update_status_batch']);
92
+ export function shouldBypassDailyRateLimit(toolName) {
93
+ return RATE_LIMIT_EXEMPT_TOOLS.has(toolName);
94
+ }
91
95
  async function resolveSessionTier(licenseKey) {
92
96
  const now = Date.now();
93
97
  const cached = sessionTierCache.get(licenseKey);
@@ -151,7 +155,7 @@ export function withUsageTracking(toolName, handler) {
151
155
  return async (args, extra) => {
152
156
  const tier = await getCurrentTier();
153
157
  // During open-access window, skip rate limiting for free tier
154
- if (tier === 'free' && !isOpenAccessActive()) {
158
+ if (tier === 'free' && !isOpenAccessActive() && !shouldBypassDailyRateLimit(toolName)) {
155
159
  const today = new Date().toISOString().slice(0, 10);
156
160
  const [trialState, callCount, lastCallAt] = await Promise.all([
157
161
  usageStore.ensureTrialState(),
@@ -63,6 +63,19 @@ async function autoDiscoverProject(projectId, projectPath, collector) {
63
63
  catch {
64
64
  /* best-effort */
65
65
  }
66
+ // SPEC-1017: list_specs enforces the strict planu/ policy in check mode.
67
+ // Mutating cleanup is handled by init/update/validate/housekeeping; list_specs
68
+ // stays read-mostly and surfaces exact offenders if any remain.
69
+ try {
70
+ const { validateStrictPlanuLayout } = await import('../engine/spec-migrator/index.js');
71
+ const layout = await validateStrictPlanuLayout(projectPath);
72
+ if (!layout.ok) {
73
+ collector.pushOk('strict-planu-layout', `Non-canonical planu/ paths detected:\n${layout.offenders.map((p) => `- ${p}`).join('\n')}`);
74
+ }
75
+ }
76
+ catch {
77
+ /* best-effort — never block list_specs */
78
+ }
66
79
  // SPEC-715: list_specs is read-only. Migrations are NEVER run here.
67
80
  // Drift is detected (read-only) and reported so the LLM can decide to run heal_spec_docs.
68
81
  try {
@@ -1,4 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare const OFFICIAL_SDD_TOOL_NAMES: readonly ["planu_status", "facilitate", "init_project", "clarify_requirements", "create_spec", "challenge_spec", "check_readiness", "update_status", "package_handoff", "validate", "reconcile_spec", "create_rule", "create_skill", "skill_search"];
2
+ export declare const OFFICIAL_SDD_TOOL_NAMES: readonly ["planu_status", "facilitate", "init_project", "clarify_requirements", "create_spec", "challenge_spec", "check_readiness", "update_status", "update_status_batch", "package_handoff", "validate", "reconcile_spec", "create_rule", "create_skill", "skill_search"];
3
3
  export declare function registerSddTools(server: McpServer): void;
4
4
  //# sourceMappingURL=register-sdd-tools.d.ts.map
@@ -21,6 +21,7 @@ export const OFFICIAL_SDD_TOOL_NAMES = [
21
21
  'challenge_spec',
22
22
  'check_readiness',
23
23
  'update_status',
24
+ 'update_status_batch',
24
25
  'package_handoff',
25
26
  'validate',
26
27
  'reconcile_spec',
@@ -10,6 +10,7 @@ import { handleClarifyRequirements } from '../clarify-requirements.js';
10
10
  import { handleCreateSpec } from '../create-spec.js';
11
11
  import { handleListSpecs } from '../list-specs.js';
12
12
  import { handleUpdateStatus } from '../update-status.js';
13
+ import { handleUpdateStatusBatch } from '../update-status/batch.js';
13
14
  import { handleEstimate } from '../estimate.js';
14
15
  import { handleReverseEngineer } from '../reverse-engineer.js';
15
16
  import { handleValidate } from '../validate.js';
@@ -304,6 +305,21 @@ export function registerCoreSpecTools(server) {
304
305
  .describe('SPEC-769: Force-approve a spec that scored below 70 on the readiness gate. Warnings are stored in spec.md frontmatter. Use when the LLM has explicitly confirmed the spec is ready despite a low readiness score.'),
305
306
  },
306
307
  }, safeTracked('update_status', async (args) => handleUpdateStatus(args)));
308
+ server.registerTool('update_status_batch', {
309
+ description: 'Batch update many specs to the same status in one MCP execution. Uses the same transition rules as update_status and reports updated/skipped/failed per spec.',
310
+ inputSchema: {
311
+ specIds: z.array(z.string().min(1).max(500)).min(1).describe('Spec IDs to update'),
312
+ projectId: z.string().max(500).optional().describe('Project ID, if known'),
313
+ projectPath: z
314
+ .string()
315
+ .max(4096)
316
+ .optional()
317
+ .describe('Absolute project root. Preferred when projectId is unknown.'),
318
+ status: SpecStatusEnum.describe('New status for all specs'),
319
+ dryRun: z.boolean().optional().describe('Preview the batch without mutating any spec.'),
320
+ reviewNotes: z.string().max(10_000).optional().describe('Optional notes for each transition.'),
321
+ },
322
+ }, safeTracked('update_status_batch', async (args) => handleUpdateStatusBatch(args)));
307
323
  // 8. estimate
308
324
  server.registerTool('estimate', {
309
325
  description: t('tools.estimate.description'),
@@ -107,7 +107,7 @@ export async function handleListLocks(input) {
107
107
  try {
108
108
  // SPEC-301: legacy manual locks (data/projects/.../locks/*.json)
109
109
  const legacyLocks = await listActiveLocks(projectPath);
110
- // SPEC-719: cross-process disk locks (planu/.locks/*.lock)
110
+ // SPEC-719: cross-process disk locks (data/.locks/planu/*.lock)
111
111
  const crossProcessLocks = await listActiveCrossProcessLocks(projectPath).catch(() => []);
112
112
  const totalCount = legacyLocks.length + crossProcessLocks.length;
113
113
  if (totalCount === 0) {
@@ -1182,11 +1182,11 @@ export function registerMiscGroupTools(s) {
1182
1182
  registerRepairFrontmatterDriftTool(s);
1183
1183
  // ── Generate dashboard ─────────────────────────────────────────────────────
1184
1184
  s.registerTool('generate_spec_dashboard', {
1185
- description: 'Regenerate planu/index.html dashboard with all specs, metrics, and pagination. Call after bulk changes or to fix a stale dashboard.',
1185
+ description: 'Deprecated compatibility tool. Planu no longer writes generated dashboard HTML into planu/.',
1186
1186
  inputSchema: {
1187
1187
  projectPath: z.string().describe('Absolute path to the project root directory'),
1188
1188
  },
1189
- annotations: { readOnlyHint: false },
1189
+ annotations: { readOnlyHint: true },
1190
1190
  }, async (args) => {
1191
1191
  const projectId = hashProjectPath(args.projectPath);
1192
1192
  const specs = await specStore.listSpecs(projectId);
@@ -1196,7 +1196,7 @@ export function registerMiscGroupTools(s) {
1196
1196
  content: [
1197
1197
  {
1198
1198
  type: 'text',
1199
- text: `Dashboard regenerated: ${String(specs.length)} specs (${String(doneCount)} done). File: planu/index.html`,
1199
+ text: `Dashboard generation is deprecated; no project files were written. Current specs: ${String(specs.length)} (${String(doneCount)} done).`,
1200
1200
  },
1201
1201
  ],
1202
1202
  };
@@ -1331,7 +1331,7 @@ export function registerMiscGroupTools(s) {
1331
1331
  // ── SPEC-739: flag_spec_gap ─────────────────────────────────────────────────
1332
1332
  s.registerTool('flag_spec_gap', {
1333
1333
  description: 'Record a spec implementation gap discovered mid-implementation. ' +
1334
- 'Persists a hash-chained entry to planu/research/gaps.jsonl with severity ' +
1334
+ 'Persists a hash-chained entry to external Planu project data with severity ' +
1335
1335
  "classification ('low' | 'medium' | 'high') and affected spec links. " +
1336
1336
  'Existing callers without severity or affectedSpecs continue to work (defaults applied).',
1337
1337
  inputSchema: {
@@ -0,0 +1,3 @@
1
+ import type { ToolResult, UpdateStatusBatchInput } from '../../types/index.js';
2
+ export declare function handleUpdateStatusBatch(input: UpdateStatusBatchInput): Promise<ToolResult>;
3
+ //# sourceMappingURL=batch.d.ts.map
@@ -0,0 +1,96 @@
1
+ // tools/update-status/batch.ts — SPEC-1017 batch status transitions
2
+ import { handleUpdateStatus } from './index.js';
3
+ import { resolveProjectIdOrAutoDetect } from '../resolve-project-id.js';
4
+ function firstText(result) {
5
+ const first = result.content[0];
6
+ return first?.type === 'text' ? first.text : JSON.stringify(result.structuredContent ?? {});
7
+ }
8
+ export async function handleUpdateStatusBatch(input) {
9
+ const uniqueSpecIds = [...new Set(input.specIds)].sort();
10
+ if (uniqueSpecIds.length === 0) {
11
+ return { content: [{ type: 'text', text: 'update_status_batch requires at least one specId.' }], isError: true };
12
+ }
13
+ const resolved = await resolveProjectIdOrAutoDetect({
14
+ projectId: input.projectId,
15
+ projectPath: input.projectPath,
16
+ });
17
+ if (!resolved.ok) {
18
+ return resolved.errorResult;
19
+ }
20
+ if (input.dryRun) {
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: `Batch status dry run: ${uniqueSpecIds.length} spec(s) would transition to ${input.status}.\n` +
26
+ uniqueSpecIds.map((id) => `- ${id}`).join('\n'),
27
+ },
28
+ ],
29
+ structuredContent: {
30
+ dryRun: true,
31
+ status: input.status,
32
+ specIds: uniqueSpecIds,
33
+ },
34
+ };
35
+ }
36
+ const updated = [];
37
+ const skipped = [];
38
+ const failed = [];
39
+ const previousSuppress = process.env.PLANU_SUPPRESS_AUTOCOMMIT;
40
+ process.env.PLANU_SUPPRESS_AUTOCOMMIT = 'true';
41
+ try {
42
+ for (const specId of uniqueSpecIds) {
43
+ const result = await handleUpdateStatus({
44
+ specId,
45
+ status: input.status,
46
+ projectId: resolved.projectId,
47
+ projectPath: resolved.projectPath,
48
+ reviewNotes: input.reviewNotes,
49
+ });
50
+ const message = firstText(result);
51
+ if (result.isError === true) {
52
+ if (/already|idempotent|same status/i.test(message)) {
53
+ skipped.push({ specId, message });
54
+ }
55
+ else {
56
+ failed.push({ specId, message });
57
+ }
58
+ }
59
+ else {
60
+ updated.push({ specId, message });
61
+ }
62
+ }
63
+ }
64
+ finally {
65
+ if (previousSuppress === undefined) {
66
+ delete process.env.PLANU_SUPPRESS_AUTOCOMMIT;
67
+ }
68
+ else {
69
+ process.env.PLANU_SUPPRESS_AUTOCOMMIT = previousSuppress;
70
+ }
71
+ }
72
+ try {
73
+ const { runStrictPlanuCleanup } = await import('../../engine/spec-migrator/index.js');
74
+ await runStrictPlanuCleanup(resolved.projectPath);
75
+ }
76
+ catch {
77
+ /* best-effort final pass */
78
+ }
79
+ const text = [
80
+ `Batch status transition complete: ${input.status}`,
81
+ `Updated: ${updated.length}`,
82
+ `Skipped: ${skipped.length}`,
83
+ `Failed: ${failed.length}`,
84
+ ].join('\n');
85
+ return {
86
+ content: [{ type: 'text', text }],
87
+ isError: failed.length > 0,
88
+ structuredContent: {
89
+ updated,
90
+ skipped,
91
+ failed,
92
+ sideEffectsFlushed: ['strict-planu-cleanup'],
93
+ },
94
+ };
95
+ }
96
+ //# sourceMappingURL=batch.js.map
@@ -264,7 +264,7 @@ export async function checkValidationReportGate(specId, projectId, force) {
264
264
  return null;
265
265
  }
266
266
  if (!result.payload.passed) {
267
- const artifactPath = `planu/data/projects/${projectId}/handoffs/${specId}/validation-report.json`;
267
+ const artifactPath = `external Planu project data: handoffs/${specId}/validation-report.json`;
268
268
  return {
269
269
  content: [
270
270
  {
@@ -86,7 +86,9 @@ export async function syncSpecFiles(updatedSpec, currentStatus, newStatus, proje
86
86
  // Commit message must reflect the actual transition — 'mark-done' is only
87
87
  // correct when newStatus === 'done'; any other transition uses 'status-update'
88
88
  // to avoid misleading "mark as done" messages on approved/implementing/etc.
89
- if (projectPath) {
89
+ if (projectPath &&
90
+ process.env.PLANU_ENABLE_AUTOCOMMIT === 'true' &&
91
+ process.env.PLANU_SUPPRESS_AUTOCOMMIT !== 'true') {
90
92
  try {
91
93
  const { git: gitCmd } = await import('../git/git-helpers.js');
92
94
  await gitCmd(projectPath, ['add', '--', updatedSpec.specPath]);
@@ -240,6 +240,16 @@ export async function handleUpdateStatus(params, server) {
240
240
  return resolved.errorResult;
241
241
  }
242
242
  const projectId = resolved.projectId;
243
+ const effectiveProjectPath = resolved.projectPath;
244
+ if (effectiveProjectPath) {
245
+ try {
246
+ const { runStrictPlanuCleanup } = await import('../../engine/spec-migrator/index.js');
247
+ await runStrictPlanuCleanup(effectiveProjectPath);
248
+ }
249
+ catch {
250
+ /* strict cleanup is best-effort here; validate fails closed if artifacts remain */
251
+ }
252
+ }
243
253
  try {
244
254
  // Get the current spec
245
255
  const spec = await specStore.getSpec(projectId, specId);
@@ -271,7 +281,7 @@ export async function handleUpdateStatus(params, server) {
271
281
  content: [
272
282
  {
273
283
  type: 'text',
274
- text: `Spec ${specId} is locked by another process (cross-process lock). Retry later or inspect planu/.locks/${specId}.lock`,
284
+ text: `Spec ${specId} is locked by another process (cross-process lock). Retry later or inspect data/.locks/planu/${specId}.lock`,
275
285
  },
276
286
  ],
277
287
  isError: true,
@@ -864,7 +874,10 @@ export async function handleUpdateStatus(params, server) {
864
874
  }
865
875
  // SPEC-544: Final git add after all writes complete (covers auto-advance multi-step transitions)
866
876
  // SPEC-575: Auto-commit staged planu/ docs (idempotent, safe-fail)
867
- if (effectiveGatePath && updatedSpec.specPath) {
877
+ if (effectiveGatePath &&
878
+ updatedSpec.specPath &&
879
+ process.env.PLANU_ENABLE_AUTOCOMMIT === 'true' &&
880
+ process.env.PLANU_SUPPRESS_AUTOCOMMIT !== 'true') {
868
881
  try {
869
882
  const { git: gitCmd } = await import('../git/git-helpers.js');
870
883
  await gitCmd(effectiveGatePath, ['add', 'planu/']);
@@ -162,22 +162,18 @@ export async function runDoneActions(projectId, specId, gitBranch) {
162
162
  }
163
163
  return null;
164
164
  })(),
165
- // SPEC-544: Auto-stage planu/ files, then warn only if non-planu changes remain
166
- // SPEC-575: Auto-commit staged planu/ docs (idempotent, safe-fail)
165
+ // Warn if non-planu changes remain. Planu autocommit/staging is opt-in only.
167
166
  (async () => {
168
167
  try {
169
168
  const { resolveProjectPath, git: gitCmd } = await import('./git/git-helpers.js');
170
169
  const projectPath = await resolveProjectPath(projectId);
171
- try {
170
+ if (process.env.PLANU_ENABLE_AUTOCOMMIT === 'true') {
172
171
  await gitCmd(projectPath, ['add', 'planu/']);
173
172
  const { planuAutoCommit } = await import('../engine/git/planu-autocommit.js');
174
173
  void planuAutoCommit({ projectPath, specId, reason: 'mark-done' }).catch((err) => {
175
174
  console.error('[update-status] planuAutoCommit failed:', err instanceof Error ? err.message : String(err));
176
175
  });
177
176
  }
178
- catch {
179
- // git add failed (nothing to stage, or not a git repo) — continue to status check
180
- }
181
177
  const { stdout } = await gitCmd(projectPath, ['status', '--porcelain']);
182
178
  const nonPlanuLines = stdout
183
179
  .trim()