@planu/cli 4.3.13 → 4.3.15
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/dist/cli/commands/audit.js +1 -1
- package/dist/cli/commands/release.d.ts +3 -0
- package/dist/cli/commands/release.js +87 -0
- package/dist/cli/commands/spec.js +37 -0
- package/dist/cli/commands/status.js +22 -2
- package/dist/cli/router.js +3 -1
- package/dist/config/elicitation-dimensions.json +2 -2
- package/dist/config/skill-templates/planu-native.md +29 -0
- package/dist/config/workflow-conventions-catalog.json +0 -8
- package/dist/engine/core-bridge.js +5 -0
- package/dist/engine/elicitation/answer-extractor.js +11 -2
- package/dist/engine/skill-generator/skill-definitions.js +4 -4
- package/dist/hosts/claude-code/ux/skills-writer.js +2 -1
- package/dist/index.js +1 -0
- package/dist/native/lightweight-command-catalog.d.ts +59 -0
- package/dist/native/lightweight-command-catalog.js +94 -0
- package/dist/tools/create-spec/autopilot-analyzer.js +20 -43
- package/dist/tools/create-spec/question-generator.js +6 -0
- package/dist/tools/create-spec.js +1 -3
- package/dist/tools/init-project/host-assets-writer.js +1 -0
- package/dist/tools/init-project/per-client-files-writer.js +33 -2
- package/dist/types/clarification.d.ts +2 -0
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/native.d.ts +33 -0
- package/dist/types/native.js +3 -0
- package/package.json +13 -11
- package/planu-native.json +82 -0
- package/planu-plugin.json +39 -0
- package/dist/config/criteria-injection-rules.json +0 -82
- package/dist/engine/acceptance-criteria-injector/criteria-filter.d.ts +0 -12
- package/dist/engine/acceptance-criteria-injector/criteria-filter.js +0 -60
- package/dist/types/criteria-injection.d.ts +0 -12
- package/dist/types/criteria-injection.js +0 -3
|
@@ -8,7 +8,7 @@ import { red } from '../colors.js';
|
|
|
8
8
|
export const auditCommand = {
|
|
9
9
|
name: 'audit',
|
|
10
10
|
description: 'Audit the current project',
|
|
11
|
-
usage: 'planu audit [--path .] [--project-id ID] [--spec SPEC-001]',
|
|
11
|
+
usage: 'planu audit [debt] [--path .] [--project-id ID] [--spec SPEC-001]',
|
|
12
12
|
async run(args, flags) {
|
|
13
13
|
const { values } = parseArgs({
|
|
14
14
|
args,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// cli/commands/release.ts — lightweight release readiness checks (SPEC-1069)
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { green, red, yellow } from '../colors.js';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const RELEASE_BRANCHES = ['main', 'develop', 'release'];
|
|
7
|
+
export const releaseCommand = {
|
|
8
|
+
name: 'release',
|
|
9
|
+
description: 'Check lightweight release readiness',
|
|
10
|
+
usage: 'planu release check [--json]',
|
|
11
|
+
async run(args, flags) {
|
|
12
|
+
const subcommand = args[0] ?? 'check';
|
|
13
|
+
if (subcommand !== 'check') {
|
|
14
|
+
process.stderr.write(`${red('Error:')} Unknown release subcommand "${subcommand}".\n`);
|
|
15
|
+
process.stderr.write(`Usage: ${releaseCommand.usage}\n`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const result = await checkReleaseReadiness(process.cwd());
|
|
20
|
+
if (flags?.json) {
|
|
21
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
22
|
+
if (!result.clean || !result.synced) {
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
writeReleaseCheck(result);
|
|
28
|
+
if (!result.clean || !result.synced) {
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
async function checkReleaseReadiness(cwd) {
|
|
34
|
+
const problems = [];
|
|
35
|
+
const status = await git(cwd, ['status', '--short']);
|
|
36
|
+
const clean = status.trim().length === 0;
|
|
37
|
+
if (!clean) {
|
|
38
|
+
problems.push('working tree has uncommitted changes');
|
|
39
|
+
}
|
|
40
|
+
const head = (await git(cwd, ['rev-parse', 'HEAD'])).trim() || null;
|
|
41
|
+
const branches = {};
|
|
42
|
+
for (const branch of RELEASE_BRANCHES) {
|
|
43
|
+
branches[branch] = await resolveRef(cwd, branch);
|
|
44
|
+
}
|
|
45
|
+
const branchValues = Object.values(branches);
|
|
46
|
+
const synced = head !== null &&
|
|
47
|
+
branchValues.every((value) => value !== null) &&
|
|
48
|
+
branchValues.every((value) => value === head);
|
|
49
|
+
if (!synced) {
|
|
50
|
+
problems.push('main, develop, and release are not all synced to HEAD');
|
|
51
|
+
}
|
|
52
|
+
return { clean, synced, head, branches, problems };
|
|
53
|
+
}
|
|
54
|
+
async function resolveRef(cwd, branch) {
|
|
55
|
+
const candidates = [branch, `origin/${branch}`];
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
try {
|
|
58
|
+
const value = (await git(cwd, ['rev-parse', candidate])).trim();
|
|
59
|
+
if (value) {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Try next candidate.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
async function git(cwd, args) {
|
|
70
|
+
const result = await execFileAsync('git', args, {
|
|
71
|
+
cwd,
|
|
72
|
+
maxBuffer: 1024 * 1024,
|
|
73
|
+
timeout: 10_000,
|
|
74
|
+
});
|
|
75
|
+
return result.stdout;
|
|
76
|
+
}
|
|
77
|
+
function writeReleaseCheck(result) {
|
|
78
|
+
if (result.clean && result.synced) {
|
|
79
|
+
process.stdout.write(`${green('Release ready.')} main, develop, and release match HEAD.\n`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write(`${yellow('Release not ready.')}\n`);
|
|
83
|
+
for (const problem of result.problems) {
|
|
84
|
+
process.stdout.write(`- ${problem}\n`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=release.js.map
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
// planu spec list [--status X] → calls handleListSpecs
|
|
6
6
|
// planu spec show SPEC-NNN → reads spec from storage
|
|
7
7
|
// planu spec status SPEC-NNN done → calls handleUpdateStatus
|
|
8
|
+
// planu spec validate SPEC-NNN → calls handleValidate
|
|
8
9
|
import { parseArgs } from 'node:util';
|
|
9
10
|
import { resolve } from 'node:path';
|
|
10
11
|
import { handleCreateSpec } from '../../tools/create-spec.js';
|
|
11
12
|
import { handleListSpecs } from '../../tools/list-specs.js';
|
|
12
13
|
import { handleUpdateStatus } from '../../tools/update-status.js';
|
|
13
14
|
import { handleUpdateStatusBatch } from '../../tools/update-status/batch.js';
|
|
15
|
+
import { handleValidate } from '../../tools/validate.js';
|
|
14
16
|
import { formatToolResult } from '../formatter.js';
|
|
15
17
|
import { detectProjectId } from '../project-detector.js';
|
|
16
18
|
import { bold, cyan, green, red, dim } from '../colors.js';
|
|
@@ -28,16 +30,48 @@ function printSpecSubcommandHelp() {
|
|
|
28
30
|
` ${'list'.padEnd(10)} ${dim('List specs [--status draft|approved|done...]')}`,
|
|
29
31
|
` ${'show'.padEnd(10)} ${dim('Show spec details (SPEC-NNN)')}`,
|
|
30
32
|
` ${'status'.padEnd(10)} ${dim('Update spec status (SPEC-NNN <status>)')}`,
|
|
33
|
+
` ${'validate'.padEnd(10)} ${dim('Validate a spec against its codebase')}`,
|
|
31
34
|
'',
|
|
32
35
|
cyan('Examples:'),
|
|
33
36
|
` planu spec create "Add login flow"`,
|
|
34
37
|
` planu spec list --status approved`,
|
|
35
38
|
` planu spec show SPEC-001`,
|
|
36
39
|
` planu spec status SPEC-001 done`,
|
|
40
|
+
` planu spec validate SPEC-001`,
|
|
37
41
|
];
|
|
38
42
|
process.stdout.write(lines.join('\n') + '\n');
|
|
39
43
|
}
|
|
40
44
|
// ---------------------------------------------------------------------------
|
|
45
|
+
// Subcommand: spec validate
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
async function runValidate(args, flags) {
|
|
48
|
+
const { positionals, values } = parseArgs({
|
|
49
|
+
args,
|
|
50
|
+
options: {
|
|
51
|
+
'project-id': { type: 'string' },
|
|
52
|
+
},
|
|
53
|
+
strict: false,
|
|
54
|
+
allowPositionals: true,
|
|
55
|
+
});
|
|
56
|
+
const specId = positionals[0];
|
|
57
|
+
if (!specId) {
|
|
58
|
+
process.stderr.write(`${red('Error:')} Spec ID is required.\nUsage: planu spec validate SPEC-NNN\n`);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const projectId = values['project-id'] ?? detectProjectId();
|
|
63
|
+
const result = await handleValidate({ specId, projectId });
|
|
64
|
+
if (result.isError) {
|
|
65
|
+
process.stderr.write(`${red(formatToolResult(result, flags))}\n`);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const output = formatToolResult(result, flags);
|
|
70
|
+
if (output) {
|
|
71
|
+
process.stdout.write(output + '\n');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
41
75
|
// Subcommand: spec create
|
|
42
76
|
// ---------------------------------------------------------------------------
|
|
43
77
|
async function runCreate(args, flags) {
|
|
@@ -258,6 +292,9 @@ export const specCommand = {
|
|
|
258
292
|
case 'status':
|
|
259
293
|
await runStatus(rest, flags);
|
|
260
294
|
break;
|
|
295
|
+
case 'validate':
|
|
296
|
+
await runValidate(rest, flags);
|
|
297
|
+
break;
|
|
261
298
|
case undefined:
|
|
262
299
|
case 'help':
|
|
263
300
|
printSpecSubcommandHelp();
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { handleUpdateStatus } from '../../tools/update-status.js';
|
|
4
4
|
import { handleUpdateStatusBatch } from '../../tools/update-status/batch.js';
|
|
5
|
+
import { handlePlanStatus } from '../../tools/status-handler.js';
|
|
5
6
|
import { formatToolResult } from '../formatter.js';
|
|
6
7
|
import { detectProjectId } from '../project-detector.js';
|
|
7
8
|
import { red, green } from '../colors.js';
|
|
8
9
|
export const statusCommand = {
|
|
9
10
|
name: 'status',
|
|
10
|
-
description: '
|
|
11
|
-
usage: 'planu status
|
|
11
|
+
description: 'Show project status or update the status of a spec',
|
|
12
|
+
usage: 'planu status [--project-path . --project-id ID] | planu status <specId> --set <status> [--project-id ID] | planu status batch --set <status> SPEC-001 SPEC-002',
|
|
12
13
|
async run(args, flags) {
|
|
13
14
|
const { values, positionals } = parseStatusArgs(args);
|
|
15
|
+
if (positionals.length === 0 && !values.set) {
|
|
16
|
+
await runProjectStatus(values, flags);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
14
19
|
const isBatch = positionals[0] === 'batch';
|
|
15
20
|
const specId = positionals[0];
|
|
16
21
|
if (!specId) {
|
|
@@ -56,6 +61,21 @@ function parseStatusArgs(args) {
|
|
|
56
61
|
});
|
|
57
62
|
return { values: parsed.values, positionals: parsed.positionals };
|
|
58
63
|
}
|
|
64
|
+
async function runProjectStatus(values, flags) {
|
|
65
|
+
const result = await handlePlanStatus({
|
|
66
|
+
projectPath: values['project-path'] ?? process.cwd(),
|
|
67
|
+
projectId: values['project-id'],
|
|
68
|
+
});
|
|
69
|
+
if (result.isError) {
|
|
70
|
+
process.stderr.write(`${red(formatToolResult(result, flags))}\n`);
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const output = formatToolResult(result, flags);
|
|
75
|
+
if (output) {
|
|
76
|
+
process.stdout.write(`${output}\n`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
59
79
|
function writeUsageError(message) {
|
|
60
80
|
process.stderr.write(`${red('Error:')} ${message}\nUsage: ${statusCommand.usage}\n`);
|
|
61
81
|
}
|
package/dist/cli/router.js
CHANGED
|
@@ -4,7 +4,7 @@ import { PLANU_VERSION } from '../config/version.js';
|
|
|
4
4
|
import { red, bold, cyan, dim } from './colors.js';
|
|
5
5
|
// Lazy-load commands to avoid importing tool handlers when not needed
|
|
6
6
|
async function loadCommands() {
|
|
7
|
-
const [{ initCommand }, { createCommand }, { listCommand }, { statusCommand }, { estimateCommand }, { validateCommand }, { dashboardCommand }, { auditCommand }, { serveCommand }, { handoffCommand }, { watchCommand }, { installCommand }, { doctorCommand }, { uninstallCommand }, { activateCommand }, { specCommand }, { healthCommand },] = await Promise.all([
|
|
7
|
+
const [{ initCommand }, { createCommand }, { listCommand }, { statusCommand }, { estimateCommand }, { validateCommand }, { dashboardCommand }, { auditCommand }, { serveCommand }, { handoffCommand }, { watchCommand }, { installCommand }, { doctorCommand }, { uninstallCommand }, { activateCommand }, { specCommand }, { healthCommand }, { releaseCommand },] = await Promise.all([
|
|
8
8
|
import('./commands/init.js'),
|
|
9
9
|
import('./commands/create.js'),
|
|
10
10
|
import('./commands/list.js'),
|
|
@@ -22,6 +22,7 @@ async function loadCommands() {
|
|
|
22
22
|
import('./commands/activate.js'),
|
|
23
23
|
import('./commands/spec.js'),
|
|
24
24
|
import('./commands/health.js'),
|
|
25
|
+
import('./commands/release.js'),
|
|
25
26
|
]);
|
|
26
27
|
const cmds = [
|
|
27
28
|
initCommand,
|
|
@@ -41,6 +42,7 @@ async function loadCommands() {
|
|
|
41
42
|
activateCommand,
|
|
42
43
|
specCommand,
|
|
43
44
|
healthCommand,
|
|
45
|
+
releaseCommand,
|
|
44
46
|
];
|
|
45
47
|
const map = new Map();
|
|
46
48
|
for (const cmd of cmds) {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"id": "database",
|
|
37
37
|
"headerKey": "questions.database.header",
|
|
38
38
|
"questionKey": "questions.database.question",
|
|
39
|
-
"
|
|
39
|
+
"requiresSignal": "hasDatabase",
|
|
40
40
|
"optionSet": "database",
|
|
41
41
|
"multiSelect": true
|
|
42
42
|
},
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"id": "uiType",
|
|
45
45
|
"headerKey": "questions.uiType.header",
|
|
46
46
|
"questionKey": "questions.uiType.question",
|
|
47
|
-
"
|
|
47
|
+
"requiresSignal": "hasUi",
|
|
48
48
|
"optionSet": "uiType",
|
|
49
49
|
"multiSelect": false
|
|
50
50
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: planu-native
|
|
3
|
+
description: Use Planu's lightweight native CLI surface before escalating to full MCP mode.
|
|
4
|
+
triggers:
|
|
5
|
+
- use planu lightweight
|
|
6
|
+
- planu native mode
|
|
7
|
+
- check planu without mcp
|
|
8
|
+
- lightweight planu status
|
|
9
|
+
version: 1.0.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /planu-native — Lightweight Planu Surface
|
|
13
|
+
|
|
14
|
+
## When to invoke
|
|
15
|
+
|
|
16
|
+
Use this skill when the user wants a fast Planu workflow that does not need the full MCP tool graph.
|
|
17
|
+
|
|
18
|
+
## Native commands
|
|
19
|
+
|
|
20
|
+
- `planu status` — compact project snapshot
|
|
21
|
+
- `planu spec create "<title>"` — create a spec
|
|
22
|
+
- `planu spec list` — list specs
|
|
23
|
+
- `planu spec validate SPEC-001` — validate one spec
|
|
24
|
+
- `planu audit debt` — read-only technical debt audit
|
|
25
|
+
- `planu release check` — local release readiness check
|
|
26
|
+
|
|
27
|
+
## Escalation
|
|
28
|
+
|
|
29
|
+
Use full MCP mode when the task needs advanced lifecycle automation, generators, governance tools, MCP resources, prompts, or multi-step orchestration.
|
|
@@ -22,14 +22,6 @@
|
|
|
22
22
|
"Working tree is clean (no uncommitted changes)"
|
|
23
23
|
],
|
|
24
24
|
"whenToUse": "End of spec only. Do NOT run during iterative development — it runs the full coverage suite (> 2 min)."
|
|
25
|
-
},
|
|
26
|
-
"propose-spec-split.sh": {
|
|
27
|
-
"name": "propose-spec-split.sh",
|
|
28
|
-
"purpose": "Analyze a spec.md and propose a split when it exceeds the 35-criteria threshold for a single implementation context.",
|
|
29
|
-
"triggerCondition": "When a spec has > 35 acceptance criteria in spec.md. Must run BEFORE starting implementation — never implement XL specs directly.",
|
|
30
|
-
"expectedOutput": "Proposed sub-spec split with suggested groupings, estimated size per sub-spec, and recommended implementation order.",
|
|
31
|
-
"preConditions": ["SPEC-XXX directory exists with spec.md", "spec.md has acceptance criteria section"],
|
|
32
|
-
"whenToUse": "Proactively at spec planning time. The /implement-spec skill should call this automatically for specs > 35 criteria."
|
|
33
25
|
}
|
|
34
26
|
},
|
|
35
27
|
|
|
@@ -104,6 +104,11 @@ let nativeModule = null;
|
|
|
104
104
|
let loadDiagnostic = { attempted: [], errors: [] };
|
|
105
105
|
let loaded = false;
|
|
106
106
|
function loadNative() {
|
|
107
|
+
if (process.env.DISABLE_NATIVE_CORE === '1') {
|
|
108
|
+
loadDiagnostic = { attempted: [], errors: [] };
|
|
109
|
+
loaded = true;
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
107
112
|
if (loaded) {
|
|
108
113
|
return nativeModule;
|
|
109
114
|
}
|
|
@@ -4,6 +4,7 @@ const FRONTEND_TERMS = /\b(component|ui|page|css|style|layout|form|button|modal|
|
|
|
4
4
|
const BACKEND_TERMS = /\b(api|endpoint|route|handler|migration|query|rpc|action|middleware)\b/;
|
|
5
5
|
const INFRA_TERMS = /\b(deploy|ci|cd|docker|pipeline|config|env)\b/;
|
|
6
6
|
const BILLING_TERMS = /\b(payment|billing|invoice|subscription|checkout|pricing|pay)\b/;
|
|
7
|
+
const DATABASE_TERMS = /\b(database|schema|migration|table|query|sql|rls|rpc|persist|persistence|data access)\b/;
|
|
7
8
|
const PAYMENT_PROVIDERS = [
|
|
8
9
|
{ name: 'Stripe', pattern: /\bstripe\b/ },
|
|
9
10
|
{ name: 'PayPal', pattern: /\bpaypal\b/ },
|
|
@@ -17,8 +18,10 @@ const PAYMENT_PROVIDERS = [
|
|
|
17
18
|
* Any signal set to true means the corresponding question can be suppressed.
|
|
18
19
|
*/
|
|
19
20
|
export function extractSignals(description) {
|
|
20
|
-
const lower = description.toLowerCase();
|
|
21
|
+
const lower = stripIncidentalReferences(description.toLowerCase());
|
|
21
22
|
const hasTarget = FRONTEND_TERMS.test(lower) || BACKEND_TERMS.test(lower) || INFRA_TERMS.test(lower);
|
|
23
|
+
const hasDatabase = DATABASE_TERMS.test(lower);
|
|
24
|
+
const hasUi = FRONTEND_TERMS.test(lower);
|
|
22
25
|
let namedProvider = null;
|
|
23
26
|
for (const { name, pattern } of PAYMENT_PROVIDERS) {
|
|
24
27
|
if (pattern.test(lower)) {
|
|
@@ -30,6 +33,12 @@ export function extractSignals(description) {
|
|
|
30
33
|
const hasBilling = BILLING_TERMS.test(lower) || namedProvider !== null;
|
|
31
34
|
const wordCount = description.trim().split(/\s+/).length;
|
|
32
35
|
const hasScope = wordCount >= 10;
|
|
33
|
-
return { hasTarget, hasBilling, namedProvider, hasScope };
|
|
36
|
+
return { hasTarget, hasBilling, hasDatabase, hasUi, namedProvider, hasScope };
|
|
37
|
+
}
|
|
38
|
+
function stripIncidentalReferences(description) {
|
|
39
|
+
return description
|
|
40
|
+
.replace(/`[^`]*(?:\/|\.\w{1,8})[^`]*`/g, ' ')
|
|
41
|
+
.replace(/\b\S+\/\S+\b/g, ' ')
|
|
42
|
+
.replace(/\b[\w-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|css|scss|py|go|rs|java|rb|php)\b/g, ' ');
|
|
34
43
|
}
|
|
35
44
|
//# sourceMappingURL=answer-extractor.js.map
|
|
@@ -327,8 +327,8 @@ Route a new workflow pattern to the correct skill or rule file.
|
|
|
327
327
|
|
|
328
328
|
1. Identify the new pattern or lesson.
|
|
329
329
|
2. Determine target: skill file, CLAUDE.md rule, or MEMORY.md.
|
|
330
|
-
3. If skill: update \`.claude/skills
|
|
331
|
-
4. If rule: update \`~/.claude/rules
|
|
330
|
+
3. If skill: update \`.claude/skills/<skill-name>/skill.md\`.
|
|
331
|
+
4. If rule: update \`~/.claude/rules/<category>.md\`.
|
|
332
332
|
5. If permanent memory: update \`MEMORY.md\`.
|
|
333
333
|
6. Commit the updated file.
|
|
334
334
|
|
|
@@ -416,8 +416,8 @@ Design and implement an MCP tool.
|
|
|
416
416
|
## Steps
|
|
417
417
|
|
|
418
418
|
1. Create spec for the tool (use \`create_spec\`).
|
|
419
|
-
2. Define input schema (Zod) in \`src/tools
|
|
420
|
-
3. Implement handler in \`src/tools
|
|
419
|
+
2. Define input schema (Zod) in \`src/tools/<tool-name>/schema.ts\`.
|
|
420
|
+
3. Implement handler in \`src/tools/<tool-name>/handler.ts\`.
|
|
421
421
|
4. Register tool in \`src/tools/index.ts\`.
|
|
422
422
|
5. Write unit + integration tests.
|
|
423
423
|
6. Run: \`{test_cmd}\`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// hosts/claude-code/ux/skills-writer.ts — SPEC-588
|
|
2
|
-
// Builds and writes .claude/skills/planu-*.md files
|
|
2
|
+
// Builds and writes .claude/skills/planu-*.md files.
|
|
3
3
|
// Idempotent: skips files whose content already matches the template.
|
|
4
4
|
import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
@@ -12,6 +12,7 @@ const SKILL_SLUGS = [
|
|
|
12
12
|
'planu-resume-work',
|
|
13
13
|
'planu-release',
|
|
14
14
|
'planu-validate',
|
|
15
|
+
'planu-native',
|
|
15
16
|
];
|
|
16
17
|
/** Resolve the canonical skill-templates directory (works after tsc compilation). */
|
|
17
18
|
function resolveSkillTemplateDir() {
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { LightweightCommand, LightweightHost, NativeSurfaceManifest } from '../types/native.js';
|
|
2
|
+
export declare const LIGHTWEIGHT_COMMANDS: readonly [{
|
|
3
|
+
readonly id: "planu.status";
|
|
4
|
+
readonly title: "Project status";
|
|
5
|
+
readonly description: "Show the compact Planu project snapshot without loading the MCP tool graph.";
|
|
6
|
+
readonly invocation: "planu status";
|
|
7
|
+
readonly hosts: readonly ["codex", "claude-code"];
|
|
8
|
+
readonly requiresMcp: false;
|
|
9
|
+
readonly requiresDaemon: false;
|
|
10
|
+
readonly mapsTo: "handlePlanStatus";
|
|
11
|
+
}, {
|
|
12
|
+
readonly id: "planu.spec.create";
|
|
13
|
+
readonly title: "Create spec";
|
|
14
|
+
readonly description: "Create a new spec through the CLI-backed SDD contract.";
|
|
15
|
+
readonly invocation: "planu spec create \"<title>\"";
|
|
16
|
+
readonly hosts: readonly ["codex", "claude-code"];
|
|
17
|
+
readonly requiresMcp: false;
|
|
18
|
+
readonly requiresDaemon: false;
|
|
19
|
+
readonly mapsTo: "handleCreateSpec";
|
|
20
|
+
}, {
|
|
21
|
+
readonly id: "planu.spec.list";
|
|
22
|
+
readonly title: "List specs";
|
|
23
|
+
readonly description: "List specs in the current project with optional status/type filters.";
|
|
24
|
+
readonly invocation: "planu spec list";
|
|
25
|
+
readonly hosts: readonly ["codex", "claude-code"];
|
|
26
|
+
readonly requiresMcp: false;
|
|
27
|
+
readonly requiresDaemon: false;
|
|
28
|
+
readonly mapsTo: "handleListSpecs";
|
|
29
|
+
}, {
|
|
30
|
+
readonly id: "planu.spec.validate";
|
|
31
|
+
readonly title: "Validate spec";
|
|
32
|
+
readonly description: "Validate a spec against the current codebase from the native CLI surface.";
|
|
33
|
+
readonly invocation: "planu spec validate SPEC-001";
|
|
34
|
+
readonly hosts: readonly ["codex", "claude-code"];
|
|
35
|
+
readonly requiresMcp: false;
|
|
36
|
+
readonly requiresDaemon: false;
|
|
37
|
+
readonly mapsTo: "handleValidate";
|
|
38
|
+
}, {
|
|
39
|
+
readonly id: "planu.audit.debt";
|
|
40
|
+
readonly title: "Audit technical debt";
|
|
41
|
+
readonly description: "Run the read-only project audit path for lightweight debt checks.";
|
|
42
|
+
readonly invocation: "planu audit debt";
|
|
43
|
+
readonly hosts: readonly ["codex", "claude-code"];
|
|
44
|
+
readonly requiresMcp: false;
|
|
45
|
+
readonly requiresDaemon: false;
|
|
46
|
+
readonly mapsTo: "handleAudit";
|
|
47
|
+
}, {
|
|
48
|
+
readonly id: "planu.release.check";
|
|
49
|
+
readonly title: "Check release readiness";
|
|
50
|
+
readonly description: "Check local branch cleanliness and main/develop/release sync readiness.";
|
|
51
|
+
readonly invocation: "planu release check";
|
|
52
|
+
readonly hosts: readonly ["codex", "claude-code"];
|
|
53
|
+
readonly requiresMcp: false;
|
|
54
|
+
readonly requiresDaemon: false;
|
|
55
|
+
readonly mapsTo: "releaseCommand";
|
|
56
|
+
}];
|
|
57
|
+
export declare function getLightweightCommands(host?: LightweightHost): readonly LightweightCommand[];
|
|
58
|
+
export declare function buildNativeSurfaceManifest(version?: string): NativeSurfaceManifest;
|
|
59
|
+
//# sourceMappingURL=lightweight-command-catalog.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// native/lightweight-command-catalog.ts — SPEC-1069 native/plugin-lite command surface.
|
|
2
|
+
import { PLANU_VERSION } from '../config/version.js';
|
|
3
|
+
export const LIGHTWEIGHT_COMMANDS = [
|
|
4
|
+
{
|
|
5
|
+
id: 'planu.status',
|
|
6
|
+
title: 'Project status',
|
|
7
|
+
description: 'Show the compact Planu project snapshot without loading the MCP tool graph.',
|
|
8
|
+
invocation: 'planu status',
|
|
9
|
+
hosts: ['codex', 'claude-code'],
|
|
10
|
+
requiresMcp: false,
|
|
11
|
+
requiresDaemon: false,
|
|
12
|
+
mapsTo: 'handlePlanStatus',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'planu.spec.create',
|
|
16
|
+
title: 'Create spec',
|
|
17
|
+
description: 'Create a new spec through the CLI-backed SDD contract.',
|
|
18
|
+
invocation: 'planu spec create "<title>"',
|
|
19
|
+
hosts: ['codex', 'claude-code'],
|
|
20
|
+
requiresMcp: false,
|
|
21
|
+
requiresDaemon: false,
|
|
22
|
+
mapsTo: 'handleCreateSpec',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'planu.spec.list',
|
|
26
|
+
title: 'List specs',
|
|
27
|
+
description: 'List specs in the current project with optional status/type filters.',
|
|
28
|
+
invocation: 'planu spec list',
|
|
29
|
+
hosts: ['codex', 'claude-code'],
|
|
30
|
+
requiresMcp: false,
|
|
31
|
+
requiresDaemon: false,
|
|
32
|
+
mapsTo: 'handleListSpecs',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'planu.spec.validate',
|
|
36
|
+
title: 'Validate spec',
|
|
37
|
+
description: 'Validate a spec against the current codebase from the native CLI surface.',
|
|
38
|
+
invocation: 'planu spec validate SPEC-001',
|
|
39
|
+
hosts: ['codex', 'claude-code'],
|
|
40
|
+
requiresMcp: false,
|
|
41
|
+
requiresDaemon: false,
|
|
42
|
+
mapsTo: 'handleValidate',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'planu.audit.debt',
|
|
46
|
+
title: 'Audit technical debt',
|
|
47
|
+
description: 'Run the read-only project audit path for lightweight debt checks.',
|
|
48
|
+
invocation: 'planu audit debt',
|
|
49
|
+
hosts: ['codex', 'claude-code'],
|
|
50
|
+
requiresMcp: false,
|
|
51
|
+
requiresDaemon: false,
|
|
52
|
+
mapsTo: 'handleAudit',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'planu.release.check',
|
|
56
|
+
title: 'Check release readiness',
|
|
57
|
+
description: 'Check local branch cleanliness and main/develop/release sync readiness.',
|
|
58
|
+
invocation: 'planu release check',
|
|
59
|
+
hosts: ['codex', 'claude-code'],
|
|
60
|
+
requiresMcp: false,
|
|
61
|
+
requiresDaemon: false,
|
|
62
|
+
mapsTo: 'releaseCommand',
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
export function getLightweightCommands(host) {
|
|
66
|
+
if (host === undefined) {
|
|
67
|
+
return LIGHTWEIGHT_COMMANDS;
|
|
68
|
+
}
|
|
69
|
+
return LIGHTWEIGHT_COMMANDS.filter((command) => command.hosts.includes(host));
|
|
70
|
+
}
|
|
71
|
+
export function buildNativeSurfaceManifest(version = PLANU_VERSION) {
|
|
72
|
+
return {
|
|
73
|
+
name: 'dev.planu.native',
|
|
74
|
+
displayName: 'Planu Native Lightweight Surface',
|
|
75
|
+
version,
|
|
76
|
+
packageName: '@planu/cli',
|
|
77
|
+
modes: {
|
|
78
|
+
lightweight: {
|
|
79
|
+
requiresMcp: false,
|
|
80
|
+
requiresDaemon: false,
|
|
81
|
+
hosts: ['codex', 'claude-code'],
|
|
82
|
+
commands: LIGHTWEIGHT_COMMANDS,
|
|
83
|
+
fullMcpFallback: 'Use full MCP mode for advanced lifecycle mutations, generators, governance, and automation beyond the curated lightweight commands.',
|
|
84
|
+
},
|
|
85
|
+
fullMcp: {
|
|
86
|
+
requiresMcp: true,
|
|
87
|
+
requiresDaemon: false,
|
|
88
|
+
manifest: 'planu-plugin.json',
|
|
89
|
+
useWhen: 'Use full MCP mode when the task needs the complete Planu tool graph, MCP resources, prompts, or multi-step automation.',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=lightweight-command-catalog.js.map
|
|
@@ -71,7 +71,6 @@ export async function analyzeProjectForSpec(projectPath, description, title, kno
|
|
|
71
71
|
// Detect stack patterns and suggest relevant criteria
|
|
72
72
|
if (knowledge) {
|
|
73
73
|
result.detectedPatterns = detectPatterns(knowledge, description);
|
|
74
|
-
result.suggestedCriteria = generatePatternCriteria(result.detectedPatterns, knowledge);
|
|
75
74
|
}
|
|
76
75
|
return result;
|
|
77
76
|
}
|
|
@@ -139,16 +138,32 @@ function inferTestPath(srcPath) {
|
|
|
139
138
|
function detectPatterns(knowledge, description) {
|
|
140
139
|
const patterns = [];
|
|
141
140
|
const desc = description.toLowerCase();
|
|
142
|
-
|
|
141
|
+
const hasDatabaseIntent = desc.includes('data') ||
|
|
143
142
|
desc.includes('database') ||
|
|
144
143
|
desc.includes('tabla') ||
|
|
145
|
-
desc.includes('schema')
|
|
144
|
+
desc.includes('schema') ||
|
|
145
|
+
desc.includes('rls') ||
|
|
146
|
+
desc.includes('rpc') ||
|
|
147
|
+
desc.includes('migration') ||
|
|
148
|
+
desc.includes('persistence') ||
|
|
149
|
+
desc.includes('data access');
|
|
150
|
+
const hasUiIntent = desc.includes('component') ||
|
|
151
|
+
desc.includes('ui') ||
|
|
152
|
+
desc.includes('page') ||
|
|
153
|
+
desc.includes('screen') ||
|
|
154
|
+
desc.includes('form') ||
|
|
155
|
+
desc.includes('layout') ||
|
|
156
|
+
desc.includes('style') ||
|
|
157
|
+
desc.includes('button') ||
|
|
158
|
+
desc.includes('modal') ||
|
|
159
|
+
desc.includes('dialog');
|
|
160
|
+
if (hasDatabaseIntent) {
|
|
146
161
|
patterns.push('database');
|
|
147
162
|
}
|
|
148
163
|
if (knowledge.framework?.toLowerCase().includes('next') && desc.includes('page')) {
|
|
149
164
|
patterns.push('nextjs-pages');
|
|
150
165
|
}
|
|
151
|
-
if (knowledge.framework?.toLowerCase().includes('react')) {
|
|
166
|
+
if (knowledge.framework?.toLowerCase().includes('react') && hasUiIntent) {
|
|
152
167
|
patterns.push('react-components');
|
|
153
168
|
}
|
|
154
169
|
if (desc.includes('api') || desc.includes('endpoint')) {
|
|
@@ -160,7 +175,7 @@ function detectPatterns(knowledge, description) {
|
|
|
160
175
|
if (desc.includes('test') || desc.includes('coverage')) {
|
|
161
176
|
patterns.push('testing');
|
|
162
177
|
}
|
|
163
|
-
if (knowledge.stack.some((s) => s.toLowerCase().includes('supabase'))) {
|
|
178
|
+
if (knowledge.stack.some((s) => s.toLowerCase().includes('supabase')) && hasDatabaseIntent) {
|
|
164
179
|
patterns.push('supabase');
|
|
165
180
|
}
|
|
166
181
|
// SPEC-535: Detect LLM/foundation model features for EU AI Act Article 53-55 compliance
|
|
@@ -180,42 +195,4 @@ function detectPatterns(knowledge, description) {
|
|
|
180
195
|
}
|
|
181
196
|
return patterns;
|
|
182
197
|
}
|
|
183
|
-
/** Generate stack-specific criteria based on detected patterns — GIVEN/WHEN/THEN format. */
|
|
184
|
-
function generatePatternCriteria(patterns, _knowledge) {
|
|
185
|
-
const criteria = [];
|
|
186
|
-
for (const pattern of patterns) {
|
|
187
|
-
switch (pattern) {
|
|
188
|
-
case 'database':
|
|
189
|
-
criteria.push('GIVEN migration WHEN applied to the database THEN target table exists with columns [id, created_at] and correct data types');
|
|
190
|
-
break;
|
|
191
|
-
case 'supabase':
|
|
192
|
-
criteria.push('GIVEN unauthenticated request WHEN RLS policy is active on the table THEN query returns empty array instead of data');
|
|
193
|
-
criteria.push('GIVEN schema change WHEN supabase gen types runs THEN TypeScript types reflect the new columns without manual edits');
|
|
194
|
-
break;
|
|
195
|
-
case 'api-endpoint':
|
|
196
|
-
criteria.push('GIVEN POST handler WHEN called with invalid body (missing required field) THEN returns {status: 400, error: string}');
|
|
197
|
-
criteria.push('GIVEN POST handler WHEN called with valid body THEN validates input with Zod schema before processing');
|
|
198
|
-
break;
|
|
199
|
-
case 'authentication':
|
|
200
|
-
criteria.push('GIVEN endpoint WHEN called with no Authorization header THEN returns {status: 401}');
|
|
201
|
-
break;
|
|
202
|
-
case 'react-components':
|
|
203
|
-
criteria.push('GIVEN component WHEN rendered THEN has role attribute and aria-label set correctly for screen readers');
|
|
204
|
-
break;
|
|
205
|
-
case 'nextjs-pages':
|
|
206
|
-
criteria.push('GIVEN page component WHEN rendered THEN uses Server Component (no "use client" directive) where data-fetching occurs');
|
|
207
|
-
break;
|
|
208
|
-
case 'testing':
|
|
209
|
-
criteria.push('GIVEN test file WHEN run with vitest THEN coverage >= threshold defined in vitest.config for branches and lines');
|
|
210
|
-
break;
|
|
211
|
-
// SPEC-535: EU AI Act Article 53-55 compliance criteria for foundation model features
|
|
212
|
-
case 'llm-feature':
|
|
213
|
-
criteria.push('GIVEN the feature uses a foundation model WHEN the spec is implemented THEN spec.md documents in its inline Technical section: model name, provider (Anthropic/OpenAI/Google), and access date — required by EU AI Act Article 53(1)(a)');
|
|
214
|
-
criteria.push('GIVEN EU users interact with this feature WHEN the feature is deployed THEN an acceptable use policy is visible before the first AI interaction — required by EU AI Act Article 53(1)(c)');
|
|
215
|
-
criteria.push("GIVEN the feature processes user-generated content via LLM WHEN any EU user's data is involved THEN the privacy notice explicitly states LLM processing and links to the model provider's data processing terms");
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return criteria;
|
|
220
|
-
}
|
|
221
198
|
//# sourceMappingURL=autopilot-analyzer.js.map
|
|
@@ -45,6 +45,12 @@ function hasRequiredSignal(key, signals) {
|
|
|
45
45
|
if (key === 'hasBilling') {
|
|
46
46
|
return signals.hasBilling;
|
|
47
47
|
}
|
|
48
|
+
if (key === 'hasDatabase') {
|
|
49
|
+
return signals.hasDatabase;
|
|
50
|
+
}
|
|
51
|
+
if (key === 'hasUi') {
|
|
52
|
+
return signals.hasUi;
|
|
53
|
+
}
|
|
48
54
|
if (key === 'hasTarget') {
|
|
49
55
|
return signals.hasTarget;
|
|
50
56
|
}
|
|
@@ -24,7 +24,6 @@ import { analyzeProjectForSpec, getEmptyAutopilotResult, } from './create-spec/a
|
|
|
24
24
|
import { AutopilotSummaryCollector } from '../engine/autopilot/summary-collector.js';
|
|
25
25
|
import { trackCost } from '../engine/cost-tracking/operation-tracker.js';
|
|
26
26
|
import { analyzeSimplicity } from '../engine/simplicity-detector.js';
|
|
27
|
-
import { filterCriteriaByTags } from '../engine/acceptance-criteria-injector/criteria-filter.js';
|
|
28
27
|
import { withBudget, unwrapBudget, withTotalBudget } from '../engine/timing/budget.js';
|
|
29
28
|
import { measureStep } from '../engine/timing/structured-log.js';
|
|
30
29
|
import { resolveProjectIdOrAutoDetect } from './resolve-project-id.js';
|
|
@@ -514,8 +513,7 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
514
513
|
}
|
|
515
514
|
// Create spec directory and write lean files (SPEC-461)
|
|
516
515
|
await measureStep('mkdir-specDir', () => mkdir(specDir, { recursive: true }));
|
|
517
|
-
|
|
518
|
-
const filteredCriteria = await filterCriteriaByTags(autopilot.suggestedCriteria, spec.tags, spec.target, spec.scope).catch(() => autopilot.suggestedCriteria);
|
|
516
|
+
const filteredCriteria = autopilot.suggestedCriteria;
|
|
519
517
|
const technologyContract = await readTechnologySelectionContract(params.projectPath ?? '');
|
|
520
518
|
const contractNote = technologyContract
|
|
521
519
|
? [
|
|
@@ -64,6 +64,27 @@ Planu enforces Spec Driven Development — spec first, then implement, then vali
|
|
|
64
64
|
- NEVER write production code before spec is \`approved\`
|
|
65
65
|
- NEVER skip \`validate\` after implementation
|
|
66
66
|
`;
|
|
67
|
+
const CODEX_NATIVE_SKILL_CONTENT = `# planu-native — Planu Lightweight Native Surface
|
|
68
|
+
|
|
69
|
+
Auto-generated by \`init_project\`.
|
|
70
|
+
|
|
71
|
+
## Overview
|
|
72
|
+
|
|
73
|
+
Use Planu's lightweight CLI surface for common SDD checks without loading the full MCP tool graph.
|
|
74
|
+
|
|
75
|
+
## Native Commands
|
|
76
|
+
|
|
77
|
+
- \`planu status\` — compact project snapshot
|
|
78
|
+
- \`planu spec create "<title>"\` — create a spec
|
|
79
|
+
- \`planu spec list\` — list specs
|
|
80
|
+
- \`planu spec validate SPEC-001\` — validate one spec
|
|
81
|
+
- \`planu audit debt\` — read-only technical debt audit
|
|
82
|
+
- \`planu release check\` — local release readiness check
|
|
83
|
+
|
|
84
|
+
## Escalate to Full MCP
|
|
85
|
+
|
|
86
|
+
Use full MCP mode for advanced lifecycle automation, generators, governance tools, MCP resources, prompts, or multi-step orchestration.
|
|
87
|
+
`;
|
|
67
88
|
async function writeIfMissing(filePath, content) {
|
|
68
89
|
try {
|
|
69
90
|
await access(filePath);
|
|
@@ -84,6 +105,10 @@ async function writeCursorMdc(projectPath) {
|
|
|
84
105
|
async function writeCodexSkill(projectPath) {
|
|
85
106
|
return writeIfMissing(join(projectPath, '.agents', 'skills', 'planu-sdd.md'), CODEX_SKILL_CONTENT);
|
|
86
107
|
}
|
|
108
|
+
/** SPEC-1069: Generate .agents/skills/planu-native.md for Codex lightweight mode. */
|
|
109
|
+
async function writeCodexNativeSkill(projectPath) {
|
|
110
|
+
return writeIfMissing(join(projectPath, '.agents', 'skills', 'planu-native.md'), CODEX_NATIVE_SKILL_CONTENT);
|
|
111
|
+
}
|
|
87
112
|
/**
|
|
88
113
|
* SPEC-648: Detect LLM client and generate the appropriate config files.
|
|
89
114
|
* AGENTS.md is always assumed to be generated by generateAgentFilesIfMissing.
|
|
@@ -100,10 +125,16 @@ export async function generatePerClientFiles(projectPath) {
|
|
|
100
125
|
}
|
|
101
126
|
}
|
|
102
127
|
if (detectedClient === 'codex') {
|
|
103
|
-
const
|
|
104
|
-
|
|
128
|
+
const [sddWritten, nativeWritten] = await Promise.all([
|
|
129
|
+
writeCodexSkill(projectPath),
|
|
130
|
+
writeCodexNativeSkill(projectPath),
|
|
131
|
+
]);
|
|
132
|
+
if (sddWritten) {
|
|
105
133
|
filesWritten.push('.agents/skills/planu-sdd.md');
|
|
106
134
|
}
|
|
135
|
+
if (nativeWritten) {
|
|
136
|
+
filesWritten.push('.agents/skills/planu-native.md');
|
|
137
|
+
}
|
|
107
138
|
}
|
|
108
139
|
return { detectedClient, filesWritten };
|
|
109
140
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -236,7 +236,6 @@ export * from './observatory.js';
|
|
|
236
236
|
export * from './orphan-spec-refs.js';
|
|
237
237
|
export * from './session-safeguard.js';
|
|
238
238
|
export * from './impact-detection.js';
|
|
239
|
-
export * from './criteria-injection.js';
|
|
240
239
|
export * from './gemini.js';
|
|
241
240
|
export * from './claude-code-runtime.js';
|
|
242
241
|
export * from './claude-code-ux.js';
|
package/dist/types/index.js
CHANGED
|
@@ -233,7 +233,6 @@ export * from './observatory.js';
|
|
|
233
233
|
export * from './orphan-spec-refs.js';
|
|
234
234
|
export * from './session-safeguard.js';
|
|
235
235
|
export * from './impact-detection.js';
|
|
236
|
-
export * from './criteria-injection.js';
|
|
237
236
|
export * from './gemini.js';
|
|
238
237
|
export * from './claude-code-runtime.js';
|
|
239
238
|
export * from './claude-code-ux.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type LightweightHost = 'codex' | 'claude-code';
|
|
2
|
+
export interface LightweightCommand {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
invocation: string;
|
|
7
|
+
hosts: readonly LightweightHost[];
|
|
8
|
+
requiresMcp: boolean;
|
|
9
|
+
requiresDaemon: boolean;
|
|
10
|
+
mapsTo: string;
|
|
11
|
+
}
|
|
12
|
+
export interface NativeSurfaceManifest {
|
|
13
|
+
name: string;
|
|
14
|
+
displayName: string;
|
|
15
|
+
version: string;
|
|
16
|
+
packageName: string;
|
|
17
|
+
modes: {
|
|
18
|
+
lightweight: {
|
|
19
|
+
requiresMcp: false;
|
|
20
|
+
requiresDaemon: false;
|
|
21
|
+
hosts: readonly LightweightHost[];
|
|
22
|
+
commands: readonly LightweightCommand[];
|
|
23
|
+
fullMcpFallback: string;
|
|
24
|
+
};
|
|
25
|
+
fullMcp: {
|
|
26
|
+
requiresMcp: true;
|
|
27
|
+
requiresDaemon: false;
|
|
28
|
+
manifest: string;
|
|
29
|
+
useWhen: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=native.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.15",
|
|
4
4
|
"description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"!dist/**/*.map",
|
|
14
14
|
"!dist/engine/*.node",
|
|
15
15
|
"src/i18n/messages/",
|
|
16
|
+
"planu-native.json",
|
|
17
|
+
"planu-plugin.json",
|
|
16
18
|
"README.md",
|
|
17
19
|
"CHANGELOG.md",
|
|
18
20
|
"LICENSE"
|
|
@@ -32,14 +34,14 @@
|
|
|
32
34
|
"packageName": "@planu/core"
|
|
33
35
|
},
|
|
34
36
|
"optionalDependencies": {
|
|
35
|
-
"@planu/core-darwin-arm64": "4.3.
|
|
36
|
-
"@planu/core-darwin-x64": "4.3.
|
|
37
|
-
"@planu/core-linux-arm64-gnu": "4.3.
|
|
38
|
-
"@planu/core-linux-arm64-musl": "4.3.
|
|
39
|
-
"@planu/core-linux-x64-gnu": "4.3.
|
|
40
|
-
"@planu/core-linux-x64-musl": "4.3.
|
|
41
|
-
"@planu/core-win32-arm64-msvc": "4.3.
|
|
42
|
-
"@planu/core-win32-x64-msvc": "4.3.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.3.15",
|
|
38
|
+
"@planu/core-darwin-x64": "4.3.15",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.3.15",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.3.15",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.3.15",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.3.15",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.3.15",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.3.15"
|
|
43
45
|
},
|
|
44
46
|
"engines": {
|
|
45
47
|
"node": ">=24.0.0"
|
|
@@ -127,7 +129,7 @@
|
|
|
127
129
|
],
|
|
128
130
|
"license": "SEE LICENSE IN LICENSE",
|
|
129
131
|
"dependencies": {
|
|
130
|
-
"@anthropic-ai/sdk": "^0.
|
|
132
|
+
"@anthropic-ai/sdk": "^0.99.0",
|
|
131
133
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
132
134
|
"glob": "^13.0.6",
|
|
133
135
|
"yaml": "^2.9.0",
|
|
@@ -200,7 +202,7 @@
|
|
|
200
202
|
"tsc-alias": "^1.8.17",
|
|
201
203
|
"type-coverage": "^2.29.7",
|
|
202
204
|
"typescript": "^6.0.3",
|
|
203
|
-
"typescript-eslint": "^8.
|
|
205
|
+
"typescript-eslint": "^8.60.0",
|
|
204
206
|
"vite": "^8.0.14",
|
|
205
207
|
"vitest": "^4.1.7",
|
|
206
208
|
"vue": "^3.5.34"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dev.planu.native",
|
|
3
|
+
"displayName": "Planu Native Lightweight Surface",
|
|
4
|
+
"version": "4.3.15",
|
|
5
|
+
"packageName": "@planu/cli",
|
|
6
|
+
"modes": {
|
|
7
|
+
"lightweight": {
|
|
8
|
+
"requiresMcp": false,
|
|
9
|
+
"requiresDaemon": false,
|
|
10
|
+
"hosts": ["codex", "claude-code"],
|
|
11
|
+
"commands": [
|
|
12
|
+
{
|
|
13
|
+
"id": "planu.status",
|
|
14
|
+
"title": "Project status",
|
|
15
|
+
"description": "Show the compact Planu project snapshot without loading the MCP tool graph.",
|
|
16
|
+
"invocation": "planu status",
|
|
17
|
+
"hosts": ["codex", "claude-code"],
|
|
18
|
+
"requiresMcp": false,
|
|
19
|
+
"requiresDaemon": false,
|
|
20
|
+
"mapsTo": "handlePlanStatus"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "planu.spec.create",
|
|
24
|
+
"title": "Create spec",
|
|
25
|
+
"description": "Create a new spec through the CLI-backed SDD contract.",
|
|
26
|
+
"invocation": "planu spec create \"<title>\"",
|
|
27
|
+
"hosts": ["codex", "claude-code"],
|
|
28
|
+
"requiresMcp": false,
|
|
29
|
+
"requiresDaemon": false,
|
|
30
|
+
"mapsTo": "handleCreateSpec"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "planu.spec.list",
|
|
34
|
+
"title": "List specs",
|
|
35
|
+
"description": "List specs in the current project with optional status/type filters.",
|
|
36
|
+
"invocation": "planu spec list",
|
|
37
|
+
"hosts": ["codex", "claude-code"],
|
|
38
|
+
"requiresMcp": false,
|
|
39
|
+
"requiresDaemon": false,
|
|
40
|
+
"mapsTo": "handleListSpecs"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "planu.spec.validate",
|
|
44
|
+
"title": "Validate spec",
|
|
45
|
+
"description": "Validate a spec against the current codebase from the native CLI surface.",
|
|
46
|
+
"invocation": "planu spec validate SPEC-001",
|
|
47
|
+
"hosts": ["codex", "claude-code"],
|
|
48
|
+
"requiresMcp": false,
|
|
49
|
+
"requiresDaemon": false,
|
|
50
|
+
"mapsTo": "handleValidate"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "planu.audit.debt",
|
|
54
|
+
"title": "Audit technical debt",
|
|
55
|
+
"description": "Run the read-only project audit path for lightweight debt checks.",
|
|
56
|
+
"invocation": "planu audit debt",
|
|
57
|
+
"hosts": ["codex", "claude-code"],
|
|
58
|
+
"requiresMcp": false,
|
|
59
|
+
"requiresDaemon": false,
|
|
60
|
+
"mapsTo": "handleAudit"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "planu.release.check",
|
|
64
|
+
"title": "Check release readiness",
|
|
65
|
+
"description": "Check local branch cleanliness and main/develop/release sync readiness.",
|
|
66
|
+
"invocation": "planu release check",
|
|
67
|
+
"hosts": ["codex", "claude-code"],
|
|
68
|
+
"requiresMcp": false,
|
|
69
|
+
"requiresDaemon": false,
|
|
70
|
+
"mapsTo": "releaseCommand"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"fullMcpFallback": "Use full MCP mode for advanced lifecycle mutations, generators, governance, and automation beyond the curated lightweight commands."
|
|
74
|
+
},
|
|
75
|
+
"fullMcp": {
|
|
76
|
+
"requiresMcp": true,
|
|
77
|
+
"requiresDaemon": false,
|
|
78
|
+
"manifest": "planu-plugin.json",
|
|
79
|
+
"useWhen": "Use full MCP mode when the task needs the complete Planu tool graph, MCP resources, prompts, or multi-step automation."
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dev.planu.cli",
|
|
3
|
+
"displayName": "Planu — Spec Driven Development",
|
|
4
|
+
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
+
"version": "4.3.15",
|
|
6
|
+
"icon": "assets/plugin/icon.svg",
|
|
7
|
+
"command": ["npx", "@planu/cli@latest"],
|
|
8
|
+
"packageName": "@planu/cli",
|
|
9
|
+
"capabilities": {
|
|
10
|
+
"tools": [
|
|
11
|
+
"planu_status",
|
|
12
|
+
"facilitate",
|
|
13
|
+
"init_project",
|
|
14
|
+
"clarify_requirements",
|
|
15
|
+
"create_spec",
|
|
16
|
+
"challenge_spec",
|
|
17
|
+
"check_readiness",
|
|
18
|
+
"update_status",
|
|
19
|
+
"package_handoff",
|
|
20
|
+
"validate",
|
|
21
|
+
"reconcile_spec",
|
|
22
|
+
"create_rule",
|
|
23
|
+
"create_skill",
|
|
24
|
+
"skill_search"
|
|
25
|
+
],
|
|
26
|
+
"resources": ["planu://specs/list", "planu://specs/{id}", "planu://project/status", "planu://roadmap"],
|
|
27
|
+
"prompts": ["create-spec-from-idea", "review-spec-readiness", "generate-implementation-plan"],
|
|
28
|
+
"subagents": ["sdd-orchestrator", "spec-challenger", "test-generator"]
|
|
29
|
+
},
|
|
30
|
+
"compatibility": {
|
|
31
|
+
"minimumHostVersion": "1.0.0",
|
|
32
|
+
"requiredFeatures": ["mcp-tools", "file-editing"]
|
|
33
|
+
},
|
|
34
|
+
"repository": "https://github.com/planu-dev/planu",
|
|
35
|
+
"author": "Planu",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"homepage": "https://planu.dev",
|
|
38
|
+
"keywords": ["sdd", "spec-driven-development", "mcp", "specs", "planning", "ai", "bdd", "tdd"]
|
|
39
|
+
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 1,
|
|
3
|
-
"rules": [
|
|
4
|
-
{
|
|
5
|
-
"id": "eu-ai-act-53-1a",
|
|
6
|
-
"criterion": "GIVEN the feature uses a foundation model WHEN the spec is implemented THEN spec.md documents in its inline Technical section: model name, provider, and access date — EU AI Act Article 53(1)(a)",
|
|
7
|
-
"requiresTags": ["llm", "foundation-model", "ai-feature"],
|
|
8
|
-
"requiresTargets": [],
|
|
9
|
-
"requiresScopes": []
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"id": "eu-ai-act-53-1c",
|
|
13
|
-
"criterion": "GIVEN EU users interact with this feature WHEN the feature is deployed THEN an acceptable use policy is visible before the first AI interaction — required by EU AI Act Article 53(1)(c)",
|
|
14
|
-
"requiresTags": ["llm", "foundation-model", "ai-feature"],
|
|
15
|
-
"requiresTargets": [],
|
|
16
|
-
"requiresScopes": []
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"id": "eu-ai-act-privacy",
|
|
20
|
-
"criterion": "GIVEN the feature processes user-generated content via LLM WHEN any EU user's data is involved THEN the privacy notice explicitly states LLM processing and links to the model provider's data processing terms",
|
|
21
|
-
"requiresTags": ["llm", "foundation-model", "ai-feature"],
|
|
22
|
-
"requiresTargets": [],
|
|
23
|
-
"requiresScopes": []
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"id": "vitest-coverage",
|
|
27
|
-
"criterion": "GIVEN test file WHEN run with vitest THEN coverage >= threshold defined in vitest.config for branches and lines",
|
|
28
|
-
"requiresTags": [],
|
|
29
|
-
"requiresTargets": ["backend", "frontend", "shared", "fullstack"],
|
|
30
|
-
"requiresScopes": []
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
"id": "db-migration",
|
|
34
|
-
"criterion": "GIVEN migration WHEN applied to the database THEN target table exists with columns [id, created_at] and correct data types",
|
|
35
|
-
"requiresTags": ["database", "migration"],
|
|
36
|
-
"requiresTargets": ["database"],
|
|
37
|
-
"requiresScopes": []
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"id": "api-zod-invalid",
|
|
41
|
-
"criterion": "GIVEN POST handler WHEN called with invalid body THEN returns {status: 400, error: string}",
|
|
42
|
-
"requiresTags": ["api", "endpoint", "http"],
|
|
43
|
-
"requiresTargets": [],
|
|
44
|
-
"requiresScopes": []
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"id": "api-zod-valid",
|
|
48
|
-
"criterion": "GIVEN POST handler WHEN called with valid body THEN validates input with Zod schema before processing",
|
|
49
|
-
"requiresTags": ["api", "endpoint", "http"],
|
|
50
|
-
"requiresTargets": [],
|
|
51
|
-
"requiresScopes": []
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
"id": "otel-http-metrics",
|
|
55
|
-
"criterion": "GIVEN HTTP requests WHEN the service runs THEN histogram http.server.request.duration records P50/P95/P99 latency with http.method, http.status_code, and http.route attributes",
|
|
56
|
-
"requiresTags": ["otel", "observability", "monitoring"],
|
|
57
|
-
"requiresTargets": [],
|
|
58
|
-
"requiresScopes": []
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"id": "otel-traces",
|
|
62
|
-
"criterion": "GIVEN any inbound request WHEN processed THEN a trace span is created with service.name, trace_id, and span_id attributes exported to the configured OTel collector",
|
|
63
|
-
"requiresTags": ["otel", "observability", "monitoring"],
|
|
64
|
-
"requiresTargets": [],
|
|
65
|
-
"requiresScopes": []
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
"id": "resilience-retry",
|
|
69
|
-
"criterion": "GIVEN an external service call WHEN the call fails with a transient error THEN the client retries up to 3 times with exponential backoff before returning an error",
|
|
70
|
-
"requiresTags": ["resilience", "reliability"],
|
|
71
|
-
"requiresTargets": [],
|
|
72
|
-
"requiresScopes": []
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"id": "resilience-circuit-breaker",
|
|
76
|
-
"criterion": "GIVEN repeated failures from an external dependency WHEN the failure rate exceeds 50% in a 10s window THEN the circuit breaker opens and fast-fails subsequent calls for 30s",
|
|
77
|
-
"requiresTags": ["resilience", "reliability"],
|
|
78
|
-
"requiresTargets": [],
|
|
79
|
-
"requiresScopes": []
|
|
80
|
-
}
|
|
81
|
-
]
|
|
82
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/** Invalidate the rules cache (useful in tests). */
|
|
2
|
-
export declare function clearRulesCache(): void;
|
|
3
|
-
/**
|
|
4
|
-
* Filter a list of candidate criteria against the injection rules.
|
|
5
|
-
* Returns only criteria that pass the tag/target/scope gate.
|
|
6
|
-
*/
|
|
7
|
-
export declare function filterCriteriaByTags(candidates: string[], specTags: string[], specTarget: string, specScope: string): Promise<string[]>;
|
|
8
|
-
/**
|
|
9
|
-
* Check whether the EU AI Act criteria should be injected for this spec.
|
|
10
|
-
*/
|
|
11
|
-
export declare function shouldInjectEuAiActCriteria(specTags: string[]): Promise<boolean>;
|
|
12
|
-
//# sourceMappingURL=criteria-filter.d.ts.map
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// engine/acceptance-criteria-injector/criteria-filter.ts — SPEC-586: Tag-aware criteria filter
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { join, dirname } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
let cachedRules = null;
|
|
6
|
-
const RULES_PATH = join(dirname(fileURLToPath(import.meta.url)), '../../config/criteria-injection-rules.json');
|
|
7
|
-
async function loadRules() {
|
|
8
|
-
if (cachedRules !== null) {
|
|
9
|
-
return cachedRules;
|
|
10
|
-
}
|
|
11
|
-
try {
|
|
12
|
-
const content = await readFile(RULES_PATH, 'utf-8');
|
|
13
|
-
const parsed = JSON.parse(content);
|
|
14
|
-
cachedRules = parsed.rules;
|
|
15
|
-
return cachedRules;
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
/** Invalidate the rules cache (useful in tests). */
|
|
22
|
-
export function clearRulesCache() {
|
|
23
|
-
cachedRules = null;
|
|
24
|
-
}
|
|
25
|
-
function ruleApplies(rule, specTags, specTarget, specScope) {
|
|
26
|
-
const tagsMatch = rule.requiresTags.length === 0 || rule.requiresTags.some((t) => specTags.includes(t));
|
|
27
|
-
const targetsMatch = rule.requiresTargets.length === 0 || rule.requiresTargets.includes(specTarget);
|
|
28
|
-
const scopesMatch = rule.requiresScopes.length === 0 || rule.requiresScopes.includes(specScope);
|
|
29
|
-
return tagsMatch && targetsMatch && scopesMatch;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Filter a list of candidate criteria against the injection rules.
|
|
33
|
-
* Returns only criteria that pass the tag/target/scope gate.
|
|
34
|
-
*/
|
|
35
|
-
export async function filterCriteriaByTags(candidates, specTags, specTarget, specScope) {
|
|
36
|
-
const rules = await loadRules();
|
|
37
|
-
if (rules.length === 0) {
|
|
38
|
-
return candidates;
|
|
39
|
-
}
|
|
40
|
-
return candidates.filter((criterion) => {
|
|
41
|
-
const matchingRule = rules.find((r) => criterion.toLowerCase().includes(r.criterion.toLowerCase().slice(0, 40)) ||
|
|
42
|
-
r.criterion.toLowerCase().includes(criterion.toLowerCase().slice(0, 40)));
|
|
43
|
-
if (!matchingRule) {
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
return ruleApplies(matchingRule, specTags, specTarget, specScope);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Check whether the EU AI Act criteria should be injected for this spec.
|
|
51
|
-
*/
|
|
52
|
-
export async function shouldInjectEuAiActCriteria(specTags) {
|
|
53
|
-
const rules = await loadRules();
|
|
54
|
-
const euRule = rules.find((r) => r.id === 'eu-ai-act-53-1a');
|
|
55
|
-
if (!euRule) {
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
return euRule.requiresTags.some((t) => specTags.includes(t));
|
|
59
|
-
}
|
|
60
|
-
//# sourceMappingURL=criteria-filter.js.map
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export interface InjectionRule {
|
|
2
|
-
id: string;
|
|
3
|
-
criterion: string;
|
|
4
|
-
requiresTags: string[];
|
|
5
|
-
requiresTargets: string[];
|
|
6
|
-
requiresScopes: string[];
|
|
7
|
-
}
|
|
8
|
-
export interface RulesFile {
|
|
9
|
-
version: number;
|
|
10
|
-
rules: InjectionRule[];
|
|
11
|
-
}
|
|
12
|
-
//# sourceMappingURL=criteria-injection.d.ts.map
|