@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.
- package/CHANGELOG.md +5 -0
- 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/codex/hooks-generator.js +1 -0
- 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/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/release/postmortem-generator.d.ts +1 -1
- package/dist/engine/release/postmortem-generator.js +3 -2
- 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/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-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/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-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/git-setup.js +11 -2
- package/dist/tools/init-project/handler.js +1 -27
- package/dist/tools/init-project/migration-runner.js +8 -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/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,
|
|
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
|
};
|
|
@@ -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
|
}
|
|
@@ -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(),
|
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) {
|
|
@@ -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: '
|
|
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:
|
|
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
|
|
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
|
|
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,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 = `
|
|
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
|
|
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 &&
|
|
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
|
-
//
|
|
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
|
-
|
|
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()
|