@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.
- package/CHANGELOG.md +17 -1
- package/dist/cli/commands/spec.js +20 -1
- package/dist/cli/commands/status.js +18 -1
- package/dist/config/license-plans.json +1 -0
- package/dist/engine/ai-integration/agents-md/generator.js +4 -1
- package/dist/engine/ai-integration/cline/clinerules-generator.js +7 -2
- package/dist/engine/ai-integration/codex/agents-md-generator.js +2 -0
- package/dist/engine/ai-integration/codex/hooks-generator.js +1 -0
- package/dist/engine/ai-integration/cursor/cursorrules-generator.js +7 -2
- package/dist/engine/ai-integration/gemini/settings-generator.js +4 -1
- package/dist/engine/ai-integration/kiro/hooks-generator.js +2 -1
- package/dist/engine/ai-integration/windsurf/windsurfrules-generator.js +7 -2
- package/dist/engine/autopilot/action-registry.js +5 -14
- package/dist/engine/autopilot/state-updater.js +13 -10
- package/dist/engine/cascade-hooks/hooks/git-auto-stage.hook.js +3 -0
- package/dist/engine/cascade-hooks/hooks/html-regen.hook.js +1 -1
- package/dist/engine/cascade-hooks/hooks/status-json.hook.js +1 -1
- package/dist/engine/cascade-hooks/state-drift-detector.d.ts +1 -1
- package/dist/engine/cascade-hooks/state-drift-detector.js +15 -12
- package/dist/engine/git/planu-autocommit.d.ts +1 -0
- package/dist/engine/git/planu-autocommit.js +6 -0
- package/dist/engine/git-hook-injector.js +3 -3
- package/dist/engine/handoff-artifacts/io.js +3 -2
- package/dist/engine/handoff-packager.js +2 -1
- package/dist/engine/hooks/full-spectrum-generator.d.ts +2 -1
- package/dist/engine/hooks/full-spectrum-generator.js +5 -3
- package/dist/engine/marketplace-fetcher/anthropic-source.js +2 -0
- package/dist/engine/opencode/config-scaffold.js +4 -0
- package/dist/engine/release/postmortem-generator.d.ts +1 -1
- package/dist/engine/release/postmortem-generator.js +3 -2
- package/dist/engine/rules-generator/index.js +2 -0
- package/dist/engine/rules-reconciler.js +2 -0
- package/dist/engine/safety/cross-process-lock.js +2 -2
- package/dist/engine/session/checkpoint-writer.js +0 -1
- package/dist/engine/session-context-generator.js +4 -1
- package/dist/engine/skill-bootstrap/skill-writer.js +2 -0
- package/dist/engine/skill-generation/multi-agent-writer.js +2 -0
- package/dist/engine/spec-audit/index.js +2 -2
- package/dist/engine/spec-audit/report-writer.d.ts +1 -1
- package/dist/engine/spec-audit/report-writer.js +5 -4
- package/dist/engine/spec-language/english-only.d.ts +8 -7
- package/dist/engine/spec-language/english-only.js +27 -3
- package/dist/engine/spec-migrator/index.d.ts +1 -0
- package/dist/engine/spec-migrator/index.js +1 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +9 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.js +62 -0
- package/dist/engine/spec-migrator/planu-root-cleaner.js +18 -94
- package/dist/engine/spec-migrator/strict-planu-cleanup.d.ts +6 -0
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +199 -0
- package/dist/engine/spec-summary-html.d.ts +5 -5
- package/dist/engine/spec-summary-html.js +7 -32
- package/dist/engine/universal-rules/host-writer.js +8 -2
- package/dist/engine/universal-rules/rules/planu-english-specs.js +9 -5
- package/dist/hosts/claude-code/ux/skills-writer.js +2 -0
- package/dist/hosts/codex/config-scaffold.js +5 -0
- package/dist/hosts/gemini/config-scaffold.js +4 -0
- package/dist/storage/gaps-log.js +4 -4
- package/dist/storage/transition-log.js +3 -2
- package/dist/tools/audit-specs-drift.js +3 -3
- package/dist/tools/create-skill.js +21 -0
- package/dist/tools/create-spec/post-creation.d.ts +2 -1
- package/dist/tools/create-spec/post-creation.js +9 -11
- package/dist/tools/create-spec/spec-builder.js +1 -1
- package/dist/tools/create-spec.js +42 -18
- package/dist/tools/flag-spec-gap.d.ts +1 -1
- package/dist/tools/flag-spec-gap.js +1 -1
- package/dist/tools/generate-dashboard.js +3 -3
- package/dist/tools/housekeeping-sweep.js +16 -0
- package/dist/tools/init-project/agents-md-writer.js +2 -0
- package/dist/tools/init-project/conventions-writer.js +2 -0
- package/dist/tools/init-project/find-skills-writer.js +2 -0
- package/dist/tools/init-project/git-setup.js +11 -2
- package/dist/tools/init-project/handler.js +1 -27
- package/dist/tools/init-project/helpers.js +5 -0
- package/dist/tools/init-project/migration-runner.js +8 -0
- package/dist/tools/init-project/per-client-files-writer.js +2 -0
- package/dist/tools/init-project/planu-workflow-generator.js +2 -0
- package/dist/tools/init-project/rules-generator.js +7 -1
- package/dist/tools/init-project/rules-writer.js +3 -0
- package/dist/tools/init-project/skills-multi-teammate-review-writer.js +2 -0
- package/dist/tools/init-project/skills-writer.js +2 -0
- package/dist/tools/license-gate.d.ts +1 -0
- package/dist/tools/license-gate.js +5 -1
- package/dist/tools/list-specs.js +13 -0
- package/dist/tools/register-sdd-tools.d.ts +1 -1
- package/dist/tools/register-sdd-tools.js +1 -0
- package/dist/tools/register-spec-tools/core-spec-tools.js +16 -0
- package/dist/tools/spec-lock-handler.js +1 -1
- package/dist/tools/tool-registry/group-misc.js +4 -4
- package/dist/tools/update-status/batch.d.ts +3 -0
- package/dist/tools/update-status/batch.js +96 -0
- package/dist/tools/update-status/dod-gates.js +1 -1
- package/dist/tools/update-status/file-sync.js +3 -1
- package/dist/tools/update-status/index.js +15 -2
- package/dist/tools/update-status-actions.js +2 -6
- package/dist/tools/validate.js +27 -0
- package/dist/tools/workspace-dashboard-handler.js +6 -9
- package/dist/types/git.d.ts +1 -1
- package/dist/types/spec-format.d.ts +26 -0
- package/dist/types/spec-language.d.ts +8 -0
- package/dist/types/spec-language.js +2 -0
- 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,
|
|
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
|
-
|
|
79
|
-
|
|
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:
|
|
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
|
|
260
|
+
* Returns { pending: true } when external analysis state is absent.
|
|
240
261
|
*/
|
|
241
262
|
async function checkAnalysisStatus(projectPath, specId) {
|
|
242
263
|
try {
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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-
|
|
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
|
|
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
|
|
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: '
|
|
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:
|
|
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
|
|
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/
|
|
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
|
|
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
|
-
|
|
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(),
|
package/dist/tools/list-specs.js
CHANGED
|
@@ -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
|
|
@@ -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 (
|
|
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) {
|