@planu/cli 3.9.14 → 4.1.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 (102) hide show
  1. package/CHANGELOG.md +17 -1
  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/agents-md/generator.js +4 -1
  6. package/dist/engine/ai-integration/cline/clinerules-generator.js +7 -2
  7. package/dist/engine/ai-integration/codex/agents-md-generator.js +2 -0
  8. package/dist/engine/ai-integration/codex/hooks-generator.js +1 -0
  9. package/dist/engine/ai-integration/cursor/cursorrules-generator.js +7 -2
  10. package/dist/engine/ai-integration/gemini/settings-generator.js +4 -1
  11. package/dist/engine/ai-integration/kiro/hooks-generator.js +2 -1
  12. package/dist/engine/ai-integration/windsurf/windsurfrules-generator.js +7 -2
  13. package/dist/engine/autopilot/action-registry.js +5 -14
  14. package/dist/engine/autopilot/state-updater.js +13 -10
  15. package/dist/engine/cascade-hooks/hooks/git-auto-stage.hook.js +3 -0
  16. package/dist/engine/cascade-hooks/hooks/html-regen.hook.js +1 -1
  17. package/dist/engine/cascade-hooks/hooks/status-json.hook.js +1 -1
  18. package/dist/engine/cascade-hooks/state-drift-detector.d.ts +1 -1
  19. package/dist/engine/cascade-hooks/state-drift-detector.js +15 -12
  20. package/dist/engine/git/planu-autocommit.d.ts +1 -0
  21. package/dist/engine/git/planu-autocommit.js +6 -0
  22. package/dist/engine/git-hook-injector.js +3 -3
  23. package/dist/engine/handoff-artifacts/io.js +3 -2
  24. package/dist/engine/handoff-packager.js +2 -1
  25. package/dist/engine/hooks/full-spectrum-generator.d.ts +2 -1
  26. package/dist/engine/hooks/full-spectrum-generator.js +5 -3
  27. package/dist/engine/marketplace-fetcher/anthropic-source.js +2 -0
  28. package/dist/engine/opencode/config-scaffold.js +4 -0
  29. package/dist/engine/release/postmortem-generator.d.ts +1 -1
  30. package/dist/engine/release/postmortem-generator.js +3 -2
  31. package/dist/engine/rules-generator/index.js +2 -0
  32. package/dist/engine/rules-reconciler.js +2 -0
  33. package/dist/engine/safety/cross-process-lock.js +2 -2
  34. package/dist/engine/session/checkpoint-writer.js +0 -1
  35. package/dist/engine/session-context-generator.js +4 -1
  36. package/dist/engine/skill-bootstrap/skill-writer.js +2 -0
  37. package/dist/engine/skill-generation/multi-agent-writer.js +2 -0
  38. package/dist/engine/spec-audit/index.js +2 -2
  39. package/dist/engine/spec-audit/report-writer.d.ts +1 -1
  40. package/dist/engine/spec-audit/report-writer.js +5 -4
  41. package/dist/engine/spec-language/english-only.d.ts +8 -7
  42. package/dist/engine/spec-language/english-only.js +27 -3
  43. package/dist/engine/spec-migrator/index.d.ts +1 -0
  44. package/dist/engine/spec-migrator/index.js +1 -0
  45. package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +9 -0
  46. package/dist/engine/spec-migrator/planu-canonical-policy.js +62 -0
  47. package/dist/engine/spec-migrator/planu-root-cleaner.js +18 -94
  48. package/dist/engine/spec-migrator/strict-planu-cleanup.d.ts +6 -0
  49. package/dist/engine/spec-migrator/strict-planu-cleanup.js +199 -0
  50. package/dist/engine/spec-summary-html.d.ts +5 -5
  51. package/dist/engine/spec-summary-html.js +7 -32
  52. package/dist/engine/universal-rules/host-writer.js +8 -2
  53. package/dist/engine/universal-rules/rules/planu-english-specs.js +9 -5
  54. package/dist/hosts/claude-code/ux/skills-writer.js +2 -0
  55. package/dist/hosts/codex/config-scaffold.js +5 -0
  56. package/dist/hosts/gemini/config-scaffold.js +4 -0
  57. package/dist/storage/gaps-log.js +4 -4
  58. package/dist/storage/transition-log.js +3 -2
  59. package/dist/tools/audit-specs-drift.js +3 -3
  60. package/dist/tools/create-skill.js +21 -0
  61. package/dist/tools/create-spec/post-creation.d.ts +2 -1
  62. package/dist/tools/create-spec/post-creation.js +9 -11
  63. package/dist/tools/create-spec/spec-builder.js +1 -1
  64. package/dist/tools/create-spec.js +42 -18
  65. package/dist/tools/flag-spec-gap.d.ts +1 -1
  66. package/dist/tools/flag-spec-gap.js +1 -1
  67. package/dist/tools/generate-dashboard.js +3 -3
  68. package/dist/tools/housekeeping-sweep.js +16 -0
  69. package/dist/tools/init-project/agents-md-writer.js +2 -0
  70. package/dist/tools/init-project/conventions-writer.js +2 -0
  71. package/dist/tools/init-project/find-skills-writer.js +2 -0
  72. package/dist/tools/init-project/git-setup.js +11 -2
  73. package/dist/tools/init-project/handler.js +1 -27
  74. package/dist/tools/init-project/helpers.js +5 -0
  75. package/dist/tools/init-project/migration-runner.js +8 -0
  76. package/dist/tools/init-project/per-client-files-writer.js +2 -0
  77. package/dist/tools/init-project/planu-workflow-generator.js +2 -0
  78. package/dist/tools/init-project/rules-generator.js +7 -1
  79. package/dist/tools/init-project/rules-writer.js +3 -0
  80. package/dist/tools/init-project/skills-multi-teammate-review-writer.js +2 -0
  81. package/dist/tools/init-project/skills-writer.js +2 -0
  82. package/dist/tools/license-gate.d.ts +1 -0
  83. package/dist/tools/license-gate.js +5 -1
  84. package/dist/tools/list-specs.js +13 -0
  85. package/dist/tools/register-sdd-tools.d.ts +1 -1
  86. package/dist/tools/register-sdd-tools.js +1 -0
  87. package/dist/tools/register-spec-tools/core-spec-tools.js +16 -0
  88. package/dist/tools/spec-lock-handler.js +1 -1
  89. package/dist/tools/tool-registry/group-misc.js +4 -4
  90. package/dist/tools/update-status/batch.d.ts +3 -0
  91. package/dist/tools/update-status/batch.js +96 -0
  92. package/dist/tools/update-status/dod-gates.js +1 -1
  93. package/dist/tools/update-status/file-sync.js +3 -1
  94. package/dist/tools/update-status/index.js +15 -2
  95. package/dist/tools/update-status-actions.js +2 -6
  96. package/dist/tools/validate.js +27 -0
  97. package/dist/tools/workspace-dashboard-handler.js +6 -9
  98. package/dist/types/git.d.ts +1 -1
  99. package/dist/types/spec-format.d.ts +26 -0
  100. package/dist/types/spec-language.d.ts +8 -0
  101. package/dist/types/spec-language.js +2 -0
  102. package/package.json +20 -20
@@ -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
  };
@@ -3,6 +3,7 @@
3
3
  // SPEC-972: Added host-aware tool filtering for Codex and other MCP hosts.
4
4
  import { writeFile, mkdir, access } from 'node:fs/promises';
5
5
  import { join } from 'node:path';
6
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
6
7
  import { CORE_AGENTS_MD_TOOLS, loadHostRegistry, filterToolsByHost, formatToolsForAgentsMd, } from '../../engine/host-tool-filter.js';
7
8
  const AGENTS_MD_PATH = 'AGENTS.md';
8
9
  const CURSORRULES_PATH = '.cursorrules';
@@ -89,6 +90,7 @@ async function writeIfMissing(path, content) {
89
90
  return false;
90
91
  }
91
92
  catch {
93
+ assertEnglishOnlyArtifactText(content, 'agent');
92
94
  await writeFile(path, content, 'utf-8');
93
95
  return true;
94
96
  }
@@ -2,6 +2,7 @@
2
2
  // with project-specific conventions detected by init_project
3
3
  import { writeFile, mkdir, access } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  const CONVENTIONS_MD_PATH = '.claude/rules/conventions.md';
6
7
  function buildConventionsContent(knowledge) {
7
8
  const stackItems = knowledge.stack;
@@ -44,6 +45,7 @@ export async function generateConventionsMdIfMissing(projectPath, knowledge) {
44
45
  const rulesDir = join(projectPath, '.claude/rules');
45
46
  await mkdir(rulesDir, { recursive: true });
46
47
  const content = buildConventionsContent(knowledge);
48
+ assertEnglishOnlyArtifactText(content, 'rule');
47
49
  await writeFile(conventionsPath, content, 'utf-8');
48
50
  return true;
49
51
  }
@@ -2,6 +2,7 @@
2
2
  // Single hardcoded entry point for skill discovery in generated config files
3
3
  import { writeFile, mkdir, access } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  const FIND_SKILLS_PATH = '.claude/skills/find-skills.md';
6
7
  const FIND_SKILLS_CONTENT = `# /find-skills — Discover Available Workflows
7
8
 
@@ -42,6 +43,7 @@ export async function generateFindSkillsIfMissing(projectPath) {
42
43
  catch {
43
44
  const skillsDir = join(projectPath, '.claude', 'skills');
44
45
  await mkdir(skillsDir, { recursive: true });
46
+ assertEnglishOnlyArtifactText(FIND_SKILLS_CONTENT, 'skill');
45
47
  await writeFile(skillPath, FIND_SKILLS_CONTENT, 'utf-8');
46
48
  return true;
47
49
  }
@@ -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
  }
@@ -1,4 +1,5 @@
1
1
  import { detectLegalFramework, detectThirdParties, buildDefaultPrivacyConfig, } from '../../engine/pii-detector.js';
2
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
2
3
  import { readFile, writeFile, access, stat } from 'node:fs/promises';
3
4
  import { join } from 'node:path';
4
5
  import { glob } from 'glob';
@@ -283,6 +284,10 @@ export async function injectSddIntoClaude(projectPath, projectSections) {
283
284
  if (updated === content) {
284
285
  continue; // nothing changed for this file
285
286
  }
287
+ assertEnglishOnlyArtifactText(SDD_BLOCK, 'agent');
288
+ if (projectSections) {
289
+ assertEnglishOnlyArtifactText(projectSections, 'agent');
290
+ }
286
291
  await writeFile(filePath, updated, 'utf-8');
287
292
  anyModified = true;
288
293
  }
@@ -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). */
@@ -2,6 +2,7 @@
2
2
  import { writeFile, mkdir, access } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { detectAndCacheClient } from '../../engine/client-detection.js';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  const CURSOR_MDC_CONTENT = `---
6
7
  description: Planu SDD workflow for Cursor
7
8
  globs: ["**/*"]
@@ -70,6 +71,7 @@ async function writeIfMissing(filePath, content) {
70
71
  }
71
72
  catch {
72
73
  await mkdir(join(filePath, '..'), { recursive: true });
74
+ assertEnglishOnlyArtifactText(content, filePath.includes('/skills/') ? 'skill' : 'agent');
73
75
  await writeFile(filePath, content, 'utf-8');
74
76
  return true;
75
77
  }
@@ -2,6 +2,7 @@
2
2
  // SPEC-263: Autonomous CLAUDE.md that makes Planu mandatory
3
3
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
4
4
  import { dirname } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  const PLANU_WORKFLOW_MARKER = '<!-- planu-sdd-workflow -->';
6
7
  const PLANU_WORKFLOW_CONTENT = `<!-- planu-sdd-workflow -->
7
8
  ## Planu SDD Workflow (MANDATORY)
@@ -61,6 +62,7 @@ export async function injectPlanuSection(claudeMdPath, section) {
61
62
  await mkdir(dirname(claudeMdPath), { recursive: true });
62
63
  }
63
64
  const updated = injectOrReplacePlanuBlock(existing, section.content, section.marker);
65
+ assertEnglishOnlyArtifactText(section.content, 'agent');
64
66
  await writeFile(claudeMdPath, updated, 'utf-8');
65
67
  }
66
68
  /**
@@ -2,6 +2,7 @@
2
2
  // SPEC-263: Autonomous CLAUDE.md that makes Planu mandatory
3
3
  import { writeFile, mkdir, access } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  const PLANU_WORKFLOW_RULES_PATH = '.claude/rules/planu-workflow.md';
6
7
  const PLANU_WORKFLOW_RULES_CONTENT = `# Planu SDD Workflow (MANDATORY)
7
8
 
@@ -141,7 +142,9 @@ export async function generateAgentTeamsRules(projectPath) {
141
142
  const rulesPath = join(projectPath, AGENT_TEAMS_RULES_PATH);
142
143
  const rulesDir = join(projectPath, '.claude/rules');
143
144
  await mkdir(rulesDir, { recursive: true });
144
- await writeFile(rulesPath, generateAgentTeamsRulesContent(), 'utf-8');
145
+ const content = generateAgentTeamsRulesContent();
146
+ assertEnglishOnlyArtifactText(content, 'rule');
147
+ await writeFile(rulesPath, content, 'utf-8');
145
148
  }
146
149
  /**
147
150
  * Write .claude/rules/agent-teams.md only if it does not exist yet.
@@ -166,6 +169,7 @@ export async function generateWorkflowRules(projectPath) {
166
169
  const rulesPath = join(projectPath, PLANU_WORKFLOW_RULES_PATH);
167
170
  const rulesDir = join(projectPath, '.claude/rules');
168
171
  await mkdir(rulesDir, { recursive: true });
172
+ assertEnglishOnlyArtifactText(PLANU_WORKFLOW_RULES_CONTENT, 'rule');
169
173
  await writeFile(rulesPath, PLANU_WORKFLOW_RULES_CONTENT, 'utf-8');
170
174
  }
171
175
  /**
@@ -227,6 +231,7 @@ export async function generateModeRulesIfMissing(projectPath) {
227
231
  catch {
228
232
  const rulesDir = join(projectPath, '.claude/rules');
229
233
  await mkdir(rulesDir, { recursive: true });
234
+ assertEnglishOnlyArtifactText(PLANU_MODES_RULES_CONTENT, 'rule');
230
235
  await writeFile(rulesPath, PLANU_MODES_RULES_CONTENT, 'utf-8');
231
236
  return true;
232
237
  }
@@ -254,6 +259,7 @@ export async function generateResponseStyleRulesIfMissing(projectPath) {
254
259
  catch {
255
260
  const rulesDir = join(projectPath, '.claude/rules');
256
261
  await mkdir(rulesDir, { recursive: true });
262
+ assertEnglishOnlyArtifactText(RESPONSE_STYLE_RULES_CONTENT, 'rule');
257
263
  await writeFile(rulesPath, RESPONSE_STYLE_RULES_CONTENT, 'utf-8');
258
264
  return true;
259
265
  }
@@ -2,6 +2,7 @@ import { generateRules, detectAgentPlatformFromFiles } from '../../engine/skill-
2
2
  import { knowledgeStore } from '../../storage/index.js';
3
3
  import { writeFile, access, mkdir } from 'node:fs/promises';
4
4
  import { join, dirname } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  /**
6
7
  * Detect agent platform, generate rules, and write them to disk.
7
8
  * Only writes files that don't exist yet — never overwrites user custom rules.
@@ -22,6 +23,7 @@ export async function runRulesWriter(projectPath, projectId, knowledge, recommen
22
23
  }
23
24
  catch {
24
25
  await mkdir(dirname(rulesFilePath), { recursive: true });
26
+ assertEnglishOnlyArtifactText(generatedRules.rulesFile.content, 'rule');
25
27
  await writeFile(rulesFilePath, generatedRules.rulesFile.content, 'utf-8');
26
28
  rulesWritten = true;
27
29
  }
@@ -34,6 +36,7 @@ export async function runRulesWriter(projectPath, projectId, knowledge, recommen
34
36
  }
35
37
  catch {
36
38
  await mkdir(dirname(filePath), { recursive: true });
39
+ assertEnglishOnlyArtifactText(file.content, file.path.includes('skill') ? 'skill' : 'rule');
37
40
  await writeFile(filePath, file.content, 'utf-8');
38
41
  additionalFilesWritten.push(file.path);
39
42
  }
@@ -3,6 +3,7 @@
3
3
  import { writeFile, mkdir, access, readFile } from 'node:fs/promises';
4
4
  import { join, dirname } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
6
7
  const SKILL_TARGET_PATH = '.claude/skills/planu-multi-teammate-review.md';
7
8
  // Resolve the template path relative to this file (works after tsc compilation too)
8
9
  function resolveTemplatePath() {
@@ -48,6 +49,7 @@ export async function generateMultiTeammateReviewSkillIfMissing(projectPath) {
48
49
  const skillsDir = join(projectPath, '.claude/skills');
49
50
  await mkdir(skillsDir, { recursive: true });
50
51
  const content = await readTemplate();
52
+ assertEnglishOnlyArtifactText(content, 'skill');
51
53
  await writeFile(skillPath, content, 'utf-8');
52
54
  return true;
53
55
  }
@@ -2,6 +2,7 @@
2
2
  // SPEC-519: Generate compact skill file for dense SDD mode
3
3
  import { writeFile, mkdir, access } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  const COMPACT_SKILL_PATH = '.claude/skills/compact.md';
6
7
  const COMPACT_SKILL_CONTENT = `# /compact — Dense SDD Mode
7
8
 
@@ -43,6 +44,7 @@ export async function generateCompactSkillIfMissing(projectPath) {
43
44
  catch {
44
45
  const skillsDir = join(projectPath, '.claude/skills');
45
46
  await mkdir(skillsDir, { recursive: true });
47
+ assertEnglishOnlyArtifactText(COMPACT_SKILL_CONTENT, 'skill');
46
48
  await writeFile(skillPath, COMPACT_SKILL_CONTENT, 'utf-8');
47
49
  return true;
48
50
  }
@@ -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) {