@planu/cli 4.7.5 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/dist/config/project-knowledge-graph.json +70 -2
  3. package/dist/engine/project-graph/builder.js +9 -5
  4. package/dist/engine/project-graph/cache.js +4 -0
  5. package/dist/engine/project-graph/extractors/decision-store-extractor.d.ts +3 -0
  6. package/dist/engine/project-graph/extractors/decision-store-extractor.js +57 -0
  7. package/dist/engine/structural-memory/architecture-snapshot.d.ts +3 -0
  8. package/dist/engine/structural-memory/architecture-snapshot.js +132 -0
  9. package/dist/engine/structural-memory/artifact-policy.d.ts +5 -0
  10. package/dist/engine/structural-memory/artifact-policy.js +17 -0
  11. package/dist/engine/structural-memory/change-impact.d.ts +3 -0
  12. package/dist/engine/structural-memory/change-impact.js +159 -0
  13. package/dist/engine/structural-memory/contracts.d.ts +2 -0
  14. package/dist/engine/structural-memory/contracts.js +2 -0
  15. package/dist/engine/structural-memory/helpers.d.ts +28 -0
  16. package/dist/engine/structural-memory/helpers.js +122 -0
  17. package/dist/engine/structural-memory/index.d.ts +7 -0
  18. package/dist/engine/structural-memory/index.js +6 -0
  19. package/dist/engine/structural-memory/query.d.ts +3 -0
  20. package/dist/engine/structural-memory/query.js +67 -0
  21. package/dist/engine/structural-memory/status.d.ts +3 -0
  22. package/dist/engine/structural-memory/status.js +30 -0
  23. package/dist/tools/tool-registry.js +3 -0
  24. package/dist/types/index.d.ts +2 -0
  25. package/dist/types/index.js +2 -0
  26. package/dist/types/project-knowledge-graph.d.ts +10 -0
  27. package/dist/types/structural-memory.d.ts +138 -0
  28. package/dist/types/structural-memory.js +2 -0
  29. package/package.json +20 -20
  30. package/planu-native.json +29 -8
  31. package/planu-plugin.json +35 -7
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [4.9.0] - 2026-06-30
2
+
3
+ ### Features
4
+ - feat(SPEC-1099): add structural memory layer
5
+
6
+ ### Chores
7
+ - chore(planu): clear released pending specs
8
+
9
+
10
+ ## [4.8.0] - 2026-06-24
11
+
12
+ ### Features
13
+ - feat(website): refine Planu landing
14
+ - feat(website): refine Planu landing
15
+
16
+
1
17
  ## [4.7.5] - 2026-06-24
2
18
 
3
19
  ### Features
@@ -4124,4 +4140,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
4124
4140
  - Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
4125
4141
  - Multi-language i18n (EN/ES/PT) for generated specs
4126
4142
  - Clean Architecture (hexagonal) — engine, tools, storage, types layers
4127
- - 10,857 tests with ≥95% coverage
4143
+ - 10,857 tests with ≥95% coverage
@@ -18,7 +18,14 @@
18
18
  "tool",
19
19
  "handoff",
20
20
  "validation",
21
- "commit"
21
+ "commit",
22
+ "adr",
23
+ "validation_evidence",
24
+ "release_risk",
25
+ "architecture_risk",
26
+ "drift",
27
+ "contract",
28
+ "git_change"
22
29
  ],
23
30
  "edgeTypes": [
24
31
  "contains",
@@ -31,7 +38,13 @@
31
38
  "has_risk",
32
39
  "uses_tool",
33
40
  "mentions",
34
- "neighbors"
41
+ "neighbors",
42
+ "affects",
43
+ "covers",
44
+ "depends_on",
45
+ "has_evidence",
46
+ "has_status",
47
+ "has_contract"
35
48
  ],
36
49
  "classifications": ["extracted", "inferred", "ambiguous"],
37
50
  "confidenceLabels": ["high", "medium", "low"],
@@ -52,6 +65,61 @@
52
65
  "(api[_-]?key|token|secret|password)\\s*[:=]\\s*[^\\s,;]+"
53
66
  ]
54
67
  },
68
+ "structuralMemory": {
69
+ "schemaVersion": "1.0.0",
70
+ "nodeTypes": [
71
+ "spec",
72
+ "criterion",
73
+ "file",
74
+ "test",
75
+ "adr",
76
+ "decision",
77
+ "validation_evidence",
78
+ "validation",
79
+ "release",
80
+ "release_risk",
81
+ "architecture_risk",
82
+ "drift",
83
+ "risk",
84
+ "tool",
85
+ "contract",
86
+ "git_change"
87
+ ],
88
+ "edgeTypes": [
89
+ "contains",
90
+ "references",
91
+ "implements",
92
+ "tests",
93
+ "validated_by",
94
+ "decides",
95
+ "has_risk",
96
+ "uses_tool",
97
+ "affects",
98
+ "covers",
99
+ "depends_on",
100
+ "has_evidence",
101
+ "has_status",
102
+ "has_contract"
103
+ ],
104
+ "response": {
105
+ "defaultLimit": 60,
106
+ "maxLimit": 200
107
+ },
108
+ "propertySchemas": {
109
+ "spec": ["specId", "status"],
110
+ "criterion": ["specId", "index"],
111
+ "file": ["path"],
112
+ "test": ["path"],
113
+ "decision": ["specId", "decisionId"],
114
+ "adr": ["specId", "decisionId"],
115
+ "validation_evidence": ["specId", "kind", "status"],
116
+ "release_risk": ["specId", "severity"],
117
+ "architecture_risk": ["path", "ruleId", "severity"],
118
+ "drift": ["specId", "status", "severity"],
119
+ "contract": ["kind", "passed"],
120
+ "git_change": ["path", "status"]
121
+ }
122
+ },
55
123
  "toolNamePatterns": [
56
124
  "package_handoff",
57
125
  "validate",
@@ -1,6 +1,7 @@
1
1
  import { collectProjectGraphSources, diffSourceHashes, hashText, loadProjectGraphPolicy, projectGraphCachePath, projectGraphPath, readProjectGraph, readProjectGraphSourceHashes, writeProjectGraph, writeProjectGraphSourceHashes, } from './cache.js';
2
2
  import { extractGitGraph, extractReleaseGraph } from './extractors/git-extractor.js';
3
3
  import { extractHandoffGraph } from './extractors/handoff-extractor.js';
4
+ import { extractDecisionStoreGraph } from './extractors/decision-store-extractor.js';
4
5
  import { extractSpecGraph } from './extractors/spec-extractor.js';
5
6
  import { extractValidationGraph } from './extractors/validation-extractor.js';
6
7
  function uniqueNodes(nodes) {
@@ -11,10 +12,7 @@ function uniqueEdges(edges) {
11
12
  }
12
13
  function validateAgainstPolicy(result, policy) {
13
14
  const nodes = result.nodes.filter((node) => policy.nodeTypes.includes(node.type));
14
- const nodeIds = new Set(nodes.map((node) => node.id));
15
- const edges = result.edges.filter((edge) => nodeIds.has(edge.from) &&
16
- nodeIds.has(edge.to) &&
17
- policy.edgeTypes.includes(edge.type) &&
15
+ const edges = result.edges.filter((edge) => policy.edgeTypes.includes(edge.type) &&
18
16
  policy.confidenceLabels.includes(edge.confidence) &&
19
17
  policy.classifications.includes(edge.classification));
20
18
  return { nodes, edges };
@@ -50,6 +48,9 @@ export async function buildProjectKnowledgeGraph(args) {
50
48
  else if (source.kind === 'handoff') {
51
49
  extracted.push(extractHandoffGraph(source, policy));
52
50
  }
51
+ else if (source.kind === 'decisions') {
52
+ extracted.push(extractDecisionStoreGraph(source, policy));
53
+ }
53
54
  else if (source.kind === 'release-events') {
54
55
  extracted.push(extractReleaseGraph(source, policy));
55
56
  }
@@ -57,7 +58,10 @@ export async function buildProjectKnowledgeGraph(args) {
57
58
  extracted.push(await extractGitGraph({ projectPath: args.projectPath, policy }));
58
59
  const validated = extracted.map((result) => validateAgainstPolicy(result, policy));
59
60
  const nodes = uniqueNodes(validated.flatMap((result) => result.nodes));
60
- const edges = uniqueEdges(validated.flatMap((result) => result.edges));
61
+ const nodeIds = new Set(nodes.map((node) => node.id));
62
+ const edges = uniqueEdges(validated
63
+ .flatMap((result) => result.edges)
64
+ .filter((edge) => nodeIds.has(edge.from) && nodeIds.has(edge.to)));
61
65
  const sourceHashes = diff.next;
62
66
  const sourceHash = hashText(JSON.stringify(sourceHashes));
63
67
  const graph = {
@@ -83,6 +83,7 @@ export async function collectProjectGraphSources(args) {
83
83
  const specFiles = await collectFiles(join(projectRoot, 'planu', 'specs'), (path) => path.endsWith('/spec.md'));
84
84
  const handoffFiles = await collectFiles(join(dataRoot, 'handoffs'), (path) => /\.(json|md)$/.test(path));
85
85
  const validationFiles = handoffFiles.filter((path) => path.includes('validation'));
86
+ const decisions = join(dataRoot, 'decisions.json');
86
87
  const transitionLog = join(dataRoot, 'transition-log.jsonl');
87
88
  const releaseEvents = join(dataRoot, '..', '..', 'release-events.jsonl');
88
89
  const raw = [
@@ -99,6 +100,9 @@ export async function collectProjectGraphSources(args) {
99
100
  ...((await fileExists(transitionLog))
100
101
  ? [{ id: 'transition-log', kind: 'transition-log', path: transitionLog }]
101
102
  : []),
103
+ ...((await fileExists(decisions))
104
+ ? [{ id: 'decisions', kind: 'decisions', path: decisions }]
105
+ : []),
102
106
  ...((await fileExists(releaseEvents))
103
107
  ? [{ id: 'release-events', kind: 'release-events', path: releaseEvents }]
104
108
  : []),
@@ -0,0 +1,3 @@
1
+ import type { ProjectGraphExtractionResult, ProjectGraphPolicy, ProjectGraphSource } from '../../../types/project-knowledge-graph.js';
2
+ export declare function extractDecisionStoreGraph(source: ProjectGraphSource, policy: ProjectGraphPolicy): ProjectGraphExtractionResult;
3
+ //# sourceMappingURL=decision-store-extractor.d.ts.map
@@ -0,0 +1,57 @@
1
+ import { safeJsonParse } from '../../../storage/base-store.js';
2
+ import { hashText } from '../cache.js';
3
+ import { redactGraphText } from '../query.js';
4
+ function isDecision(value) {
5
+ return (value !== null &&
6
+ typeof value === 'object' &&
7
+ typeof value.id === 'string' &&
8
+ typeof value.what === 'string' &&
9
+ typeof value.why === 'string');
10
+ }
11
+ function decisionsFromSource(source) {
12
+ const parsed = safeJsonParse(source.content, { decisions: [] });
13
+ return Array.isArray(parsed.decisions) ? parsed.decisions.filter(isDecision) : [];
14
+ }
15
+ function node(args) {
16
+ return {
17
+ id: `decision:${args.decision.id}`,
18
+ type: 'decision',
19
+ label: redactGraphText(`${args.decision.what}: ${args.decision.why}`, args.policy),
20
+ source: 'decision-store-extractor',
21
+ evidence: [{ path: args.source.path, hash: args.source.hash }],
22
+ metadata: {
23
+ decisionId: args.decision.id,
24
+ specId: args.decision.specId ?? null,
25
+ category: args.decision.category,
26
+ status: args.decision.status,
27
+ decidedBy: args.decision.decidedBy,
28
+ },
29
+ updatedAt: args.decision.date,
30
+ };
31
+ }
32
+ function edge(decision, source) {
33
+ if (!decision.specId) {
34
+ return null;
35
+ }
36
+ const decisionId = `decision:${decision.id}`;
37
+ return {
38
+ id: `edge:${hashText(`spec:${decision.specId}:decides:${decisionId}`)}`,
39
+ from: `spec:${decision.specId}`,
40
+ to: decisionId,
41
+ type: 'decides',
42
+ source: 'decision-store-extractor',
43
+ confidence: 'high',
44
+ classification: 'extracted',
45
+ evidence: { path: source.path, hash: source.hash },
46
+ updatedAt: decision.date,
47
+ };
48
+ }
49
+ export function extractDecisionStoreGraph(source, policy) {
50
+ const decisions = decisionsFromSource(source);
51
+ const nodes = decisions.map((decision) => node({ decision, source, policy }));
52
+ const edges = decisions
53
+ .map((decision) => edge(decision, source))
54
+ .filter((item) => item !== null);
55
+ return { nodes, edges };
56
+ }
57
+ //# sourceMappingURL=decision-store-extractor.js.map
@@ -0,0 +1,3 @@
1
+ import type { ArchitectureSnapshotInput, ArchitectureSnapshotResult } from './contracts.js';
2
+ export declare function getArchitectureSnapshot(input: ArchitectureSnapshotInput): Promise<ArchitectureSnapshotResult>;
3
+ //# sourceMappingURL=architecture-snapshot.d.ts.map
@@ -0,0 +1,132 @@
1
+ import { clampLimit, loadFreshGraph, nextActionForFreshness, pagination, resolveProjectId, safeRelativeProjectPath, structuralPolicy, } from './helpers.js';
2
+ function aspects(input) {
3
+ const requested = input.aspects ?? ['all'];
4
+ if (requested.includes('all')) {
5
+ return new Set(['packages', 'hotspots', 'boundaries', 'releaseRisks', 'tests', 'decisions']);
6
+ }
7
+ return new Set(requested);
8
+ }
9
+ function packageName(path) {
10
+ const parts = path.split('/');
11
+ return parts.length > 1 ? parts.slice(0, 2).join('/') : (parts[0] ?? path);
12
+ }
13
+ function packageAspect(nodes, limit) {
14
+ const counts = new Map();
15
+ for (const node of nodes.filter((item) => item.type === 'file' || item.type === 'test')) {
16
+ counts.set(packageName(node.label), (counts.get(packageName(node.label)) ?? 0) + 1);
17
+ }
18
+ return [...counts.entries()]
19
+ .map(([name, fileCount]) => ({ name, fileCount }))
20
+ .sort((a, b) => b.fileCount - a.fileCount)
21
+ .slice(0, limit);
22
+ }
23
+ function hotspotAspect(nodes, edges, limit) {
24
+ const degree = new Map();
25
+ for (const edge of edges) {
26
+ degree.set(edge.from, (degree.get(edge.from) ?? 0) + 1);
27
+ degree.set(edge.to, (degree.get(edge.to) ?? 0) + 1);
28
+ }
29
+ return nodes
30
+ .map((node) => ({
31
+ id: node.id,
32
+ label: node.label,
33
+ type: node.type,
34
+ degree: degree.get(node.id) ?? 0,
35
+ }))
36
+ .filter((node) => node.degree > 0)
37
+ .sort((a, b) => b.degree - a.degree)
38
+ .slice(0, limit);
39
+ }
40
+ function boundaryAspect(edges, limit) {
41
+ return edges
42
+ .filter((edge) => edge.type === 'references' || edge.type === 'implements' || edge.type === 'tests')
43
+ .map((edge) => ({ from: edge.from, to: edge.to, type: edge.type }))
44
+ .slice(0, limit);
45
+ }
46
+ function simpleNodes(nodes, types, limit) {
47
+ return nodes
48
+ .filter((node) => types.includes(node.type))
49
+ .map((node) => ({ id: node.id, label: node.label, type: node.type }))
50
+ .slice(0, limit);
51
+ }
52
+ function snapshotAspects(args) {
53
+ const result = {};
54
+ if (args.requested.has('packages')) {
55
+ result.packages = packageAspect(args.nodes, args.limit);
56
+ }
57
+ if (args.requested.has('hotspots')) {
58
+ result.hotspots = hotspotAspect(args.nodes, args.edges, args.limit);
59
+ }
60
+ if (args.requested.has('boundaries')) {
61
+ result.boundaries = boundaryAspect(args.edges, args.limit);
62
+ }
63
+ if (args.requested.has('releaseRisks')) {
64
+ result.releaseRisks = simpleNodes(args.nodes, ['risk', 'release_risk', 'architecture_risk'], args.limit).map((node) => ({ id: node.id, label: node.label, type: node.type ?? 'risk' }));
65
+ }
66
+ if (args.requested.has('tests')) {
67
+ result.tests = simpleNodes(args.nodes, ['test'], args.limit).map(({ id, label }) => ({
68
+ id,
69
+ label,
70
+ }));
71
+ }
72
+ if (args.requested.has('decisions')) {
73
+ result.decisions = simpleNodes(args.nodes, ['decision', 'adr'], args.limit).map(({ id, label }) => ({ id, label }));
74
+ }
75
+ return result;
76
+ }
77
+ export async function getArchitectureSnapshot(input) {
78
+ const policy = await structuralPolicy();
79
+ const projectId = resolveProjectId(input);
80
+ const limit = clampLimit(input.limit, policy);
81
+ const { graph, freshness } = await loadFreshGraph({
82
+ projectId,
83
+ projectPath: input.projectPath,
84
+ policy,
85
+ });
86
+ const prefix = input.pathPrefix === undefined
87
+ ? undefined
88
+ : safeRelativeProjectPath(input.projectPath, input.pathPrefix);
89
+ if (input.pathPrefix !== undefined && prefix === null) {
90
+ const page = pagination(0, limit, 0);
91
+ return {
92
+ freshness,
93
+ pagination: page,
94
+ ...page,
95
+ nextAction: {
96
+ type: 'inspect_evidence',
97
+ message: 'Rejecting unsafe pathPrefix filter; pass a project-relative path inside the repo.',
98
+ },
99
+ aspects: {},
100
+ };
101
+ }
102
+ const page = pagination(graph?.nodes.length ?? 0, limit, 0);
103
+ if (graph === null) {
104
+ return {
105
+ freshness,
106
+ pagination: page,
107
+ ...page,
108
+ nextAction: nextActionForFreshness(freshness, true),
109
+ aspects: {},
110
+ };
111
+ }
112
+ const scopedNodes = graph.nodes.filter((node) => {
113
+ if (prefix === undefined) {
114
+ return true;
115
+ }
116
+ return node.label === prefix || node.label.startsWith(`${prefix}/`);
117
+ });
118
+ const scopedIds = new Set(scopedNodes.map((node) => node.id));
119
+ const scopedEdges = graph.edges.filter((edge) => scopedIds.has(edge.from) && scopedIds.has(edge.to));
120
+ const requested = aspects(input);
121
+ const result = snapshotAspects({ nodes: scopedNodes, edges: scopedEdges, requested, limit });
122
+ const scopedPage = pagination(scopedNodes.length, limit, 0);
123
+ return {
124
+ graphVersion: graph.graphVersion,
125
+ freshness,
126
+ pagination: scopedPage,
127
+ ...scopedPage,
128
+ nextAction: nextActionForFreshness(freshness, scopedNodes.length === 0),
129
+ aspects: result,
130
+ };
131
+ }
132
+ //# sourceMappingURL=architecture-snapshot.js.map
@@ -0,0 +1,5 @@
1
+ import type { ProjectGraphPolicy } from '../../types/project-knowledge-graph.js';
2
+ import type { StructuralMemoryArtifactPolicy } from '../../types/structural-memory.js';
3
+ export declare function createStructuralMemoryArtifactPolicy(projectId: string, policy: ProjectGraphPolicy): StructuralMemoryArtifactPolicy;
4
+ export declare function isStructuralMemoryArtifactPathAllowed(path: string): boolean;
5
+ //# sourceMappingURL=artifact-policy.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { projectGraphDir, projectGraphPath } from '../project-graph/cache.js';
2
+ export function createStructuralMemoryArtifactPolicy(projectId, policy) {
3
+ const artifactRoot = projectGraphDir(projectId, policy);
4
+ return {
5
+ artifactRoot,
6
+ graphPath: projectGraphPath(projectId, policy),
7
+ projectDataDirectory: artifactRoot.split('/project-graph')[0] ?? artifactRoot,
8
+ forbiddenPathFragment: 'planu/specs',
9
+ specDirectoryPolicy: 'spec.md-only',
10
+ evidenceAndGraphArtifactsRemainOutsideSpecs: true,
11
+ };
12
+ }
13
+ export function isStructuralMemoryArtifactPathAllowed(path) {
14
+ const normalized = path.replace(/\\/g, '/');
15
+ return !normalized.includes('/planu/specs/') && !normalized.endsWith('/planu/specs');
16
+ }
17
+ //# sourceMappingURL=artifact-policy.js.map
@@ -0,0 +1,3 @@
1
+ import type { StructuralChangeImpactInput, StructuralChangeImpactResult } from './contracts.js';
2
+ export declare function detectStructuralChangeImpact(input: StructuralChangeImpactInput): Promise<StructuralChangeImpactResult>;
3
+ //# sourceMappingURL=change-impact.d.ts.map
@@ -0,0 +1,159 @@
1
+ import { execFile as execFileCb } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { clampLimit, loadFreshGraph, nextActionForFreshness, pagination, resolveProjectId, safeRelativeProjectPath, structuralPolicy, } from './helpers.js';
4
+ const execFile = promisify(execFileCb);
5
+ function safeGitRef(ref) {
6
+ return (/^[A-Za-z0-9][A-Za-z0-9._/@{}:+-]{0,200}$/.test(ref) &&
7
+ !ref.includes('..') &&
8
+ !ref.startsWith('-'));
9
+ }
10
+ function statusFromCode(code) {
11
+ if (code.includes('A')) {
12
+ return 'added';
13
+ }
14
+ if (code.includes('D')) {
15
+ return 'deleted';
16
+ }
17
+ if (code.includes('R')) {
18
+ return 'renamed';
19
+ }
20
+ if (code.includes('?')) {
21
+ return 'untracked';
22
+ }
23
+ if (code.trim().length > 0) {
24
+ return 'modified';
25
+ }
26
+ return 'unknown';
27
+ }
28
+ function parseNameStatus(stdout, projectPath) {
29
+ return stdout
30
+ .split('\n')
31
+ .map((line) => line.trimEnd())
32
+ .filter((line) => line.length > 0)
33
+ .flatMap((line) => {
34
+ const [status, ...paths] = line.split(/\s+/);
35
+ const path = paths.at(-1);
36
+ if (!status || !path) {
37
+ return [];
38
+ }
39
+ const safePath = safeRelativeProjectPath(projectPath, path);
40
+ return safePath === null ? [] : [{ path: safePath, status: statusFromCode(status) }];
41
+ });
42
+ }
43
+ function parsePorcelain(stdout, projectPath, includeUntracked) {
44
+ return stdout
45
+ .split('\n')
46
+ .map((line) => line.trimEnd())
47
+ .filter((line) => line.length > 0)
48
+ .flatMap((line) => {
49
+ const code = line.slice(0, 2);
50
+ if (!includeUntracked && code === '??') {
51
+ return [];
52
+ }
53
+ const rawPath = line.slice(3).split(' -> ').at(-1);
54
+ if (!rawPath) {
55
+ return [];
56
+ }
57
+ const safePath = safeRelativeProjectPath(projectPath, rawPath);
58
+ return safePath === null ? [] : [{ path: safePath, status: statusFromCode(code) }];
59
+ });
60
+ }
61
+ async function gitChangedFiles(input) {
62
+ if (input.changedFiles !== undefined) {
63
+ return input.changedFiles.flatMap((path) => {
64
+ const safePath = safeRelativeProjectPath(input.projectPath, path);
65
+ return safePath === null ? [] : [{ path: safePath, status: 'unknown' }];
66
+ });
67
+ }
68
+ if (input.baseRef !== undefined) {
69
+ if (!safeGitRef(input.baseRef)) {
70
+ throw new Error('Unsafe git baseRef for structural change impact.');
71
+ }
72
+ const { stdout } = await execFile('git', ['diff', '--name-status', input.baseRef, '--'], {
73
+ cwd: input.projectPath,
74
+ maxBuffer: 1024 * 1024,
75
+ });
76
+ return parseNameStatus(stdout, input.projectPath);
77
+ }
78
+ const { stdout } = await execFile('git', ['status', '--porcelain'], {
79
+ cwd: input.projectPath,
80
+ maxBuffer: 1024 * 1024,
81
+ });
82
+ return parsePorcelain(stdout, input.projectPath, input.includeUntracked ?? true);
83
+ }
84
+ function uniqueNodes(nodes) {
85
+ return [...new Map(nodes.map((node) => [node.id, node])).values()];
86
+ }
87
+ function collectAffected(graphNodes, graphEdges, changedFiles) {
88
+ const changed = new Set(changedFiles.map((file) => file.path));
89
+ const fileNodeIds = new Set(graphNodes
90
+ .filter((node) => node.type === 'file' && changed.has(node.label))
91
+ .map((node) => node.id));
92
+ const affectedIds = new Set(fileNodeIds);
93
+ for (let depth = 0; depth < 3; depth += 1) {
94
+ for (const edge of graphEdges) {
95
+ if (affectedIds.has(edge.from)) {
96
+ affectedIds.add(edge.to);
97
+ }
98
+ if (affectedIds.has(edge.to)) {
99
+ affectedIds.add(edge.from);
100
+ }
101
+ }
102
+ }
103
+ const nodes = graphNodes.filter((node) => affectedIds.has(node.id));
104
+ const ids = new Set(nodes.map((node) => node.id));
105
+ const edges = graphEdges.filter((edge) => ids.has(edge.from) && ids.has(edge.to));
106
+ return { nodes, edges };
107
+ }
108
+ export async function detectStructuralChangeImpact(input) {
109
+ const policy = await structuralPolicy();
110
+ const projectId = resolveProjectId(input);
111
+ const limit = clampLimit(input.limit, policy);
112
+ const changedFiles = await gitChangedFiles(input);
113
+ const { graph, freshness } = await loadFreshGraph({
114
+ projectId,
115
+ projectPath: input.projectPath,
116
+ policy,
117
+ });
118
+ const page = pagination(changedFiles.length, limit, 0);
119
+ if (graph === null) {
120
+ return {
121
+ freshness,
122
+ pagination: page,
123
+ ...page,
124
+ nextAction: nextActionForFreshness(freshness, true),
125
+ changedFiles: changedFiles.slice(0, limit),
126
+ affected: {
127
+ specs: [],
128
+ criteria: [],
129
+ tests: [],
130
+ decisions: [],
131
+ evidence: [],
132
+ risks: [],
133
+ releaseRisk: [],
134
+ },
135
+ affectedEdges: [],
136
+ };
137
+ }
138
+ const affected = collectAffected(graph.nodes, graph.edges, changedFiles);
139
+ const byType = (types) => uniqueNodes(affected.nodes.filter((node) => types.includes(node.type))).slice(0, limit);
140
+ return {
141
+ graphVersion: graph.graphVersion,
142
+ freshness,
143
+ pagination: page,
144
+ ...page,
145
+ nextAction: nextActionForFreshness(freshness, affected.nodes.length === 0),
146
+ changedFiles: changedFiles.slice(0, limit),
147
+ affected: {
148
+ specs: byType(['spec']),
149
+ criteria: byType(['criterion']),
150
+ tests: byType(['test']),
151
+ decisions: byType(['decision', 'adr']),
152
+ evidence: byType(['handoff', 'validation', 'validation_evidence', 'contract']),
153
+ risks: byType(['risk', 'architecture_risk', 'drift']),
154
+ releaseRisk: byType(['release', 'release_risk']),
155
+ },
156
+ affectedEdges: affected.edges.slice(0, policy.query.maxEdges),
157
+ };
158
+ }
159
+ //# sourceMappingURL=change-impact.js.map
@@ -0,0 +1,2 @@
1
+ export type * from '../../types/structural-memory.js';
2
+ //# sourceMappingURL=contracts.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=contracts.js.map
@@ -0,0 +1,28 @@
1
+ import type { ProjectGraphEdge, ProjectGraphFreshness, ProjectGraphNode, ProjectGraphPolicy } from '../../types/project-knowledge-graph.js';
2
+ import { readProjectGraph } from '../project-graph/cache.js';
3
+ import type { StructuralMemoryFreshness, StructuralMemoryNextAction, StructuralMemoryPagination } from './contracts.js';
4
+ export declare function resolveProjectId(input: {
5
+ projectId?: string;
6
+ projectPath: string;
7
+ }): string;
8
+ export declare function freshnessStatus(freshness: ProjectGraphFreshness): StructuralMemoryFreshness;
9
+ export declare function nextActionForFreshness(freshness: StructuralMemoryFreshness, empty?: boolean): StructuralMemoryNextAction;
10
+ export declare function pagination(total: number, limit: number, offset: number): StructuralMemoryPagination;
11
+ export declare function clampLimit(limit: number | undefined, policy: ProjectGraphPolicy): number;
12
+ export declare function safeRelativeProjectPath(projectPath: string, candidate: string): string | null;
13
+ export declare function loadFreshGraph(args: {
14
+ projectId: string;
15
+ projectPath: string;
16
+ policy: ProjectGraphPolicy;
17
+ }): Promise<{
18
+ graph: Awaited<ReturnType<typeof readProjectGraph>>;
19
+ freshness: StructuralMemoryFreshness;
20
+ }>;
21
+ export declare function summarizeNodes(nodes: ProjectGraphNode[]): Record<string, number>;
22
+ export declare function relatedNodeIds(nodes: ProjectGraphNode[], edges: ProjectGraphEdge[], input: {
23
+ specId?: string;
24
+ filePath?: string;
25
+ nodeId?: string;
26
+ }): Set<string>;
27
+ export declare function structuralPolicy(): Promise<ProjectGraphPolicy>;
28
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1,122 @@
1
+ import { isAbsolute, normalize, relative, resolve } from 'node:path';
2
+ import { hashProjectPath } from '../../storage/base-store.js';
3
+ import { getProjectGraphFreshness, loadProjectGraphPolicy, readProjectGraph, } from '../project-graph/cache.js';
4
+ export function resolveProjectId(input) {
5
+ return input.projectId ?? hashProjectPath(input.projectPath);
6
+ }
7
+ export function freshnessStatus(freshness) {
8
+ const status = freshness.reason === 'missing'
9
+ ? 'missing'
10
+ : freshness.reason === 'corrupt'
11
+ ? 'corrupt'
12
+ : freshness.stale
13
+ ? 'stale'
14
+ : 'fresh';
15
+ return {
16
+ status,
17
+ graphPath: freshness.graphPath,
18
+ generatedAt: freshness.generatedAt,
19
+ changedSources: freshness.changedSources,
20
+ reason: freshness.reason,
21
+ };
22
+ }
23
+ export function nextActionForFreshness(freshness, empty = false) {
24
+ if (freshness.status === 'missing' || freshness.status === 'corrupt') {
25
+ return {
26
+ type: 'rebuild_graph',
27
+ message: 'Rebuild the project graph before trusting structural memory results.',
28
+ };
29
+ }
30
+ if (freshness.status === 'stale' || freshness.status === 'partial') {
31
+ return {
32
+ type: 'run_validate',
33
+ message: 'Run validation or rebuild graph context before using this result as gate evidence.',
34
+ };
35
+ }
36
+ if (empty) {
37
+ return {
38
+ type: 'broaden_filters',
39
+ message: 'No structural memory nodes matched; broaden filters or inspect evidence coverage.',
40
+ };
41
+ }
42
+ return { type: 'none', message: 'Structural memory result is ready to use.' };
43
+ }
44
+ export function pagination(total, limit, offset) {
45
+ const hasMore = offset + limit < total;
46
+ return {
47
+ total,
48
+ limit,
49
+ offset,
50
+ hasMore,
51
+ has_more: hasMore,
52
+ truncated: hasMore,
53
+ };
54
+ }
55
+ export function clampLimit(limit, policy) {
56
+ const defaultLimit = policy.structuralMemory?.response.defaultLimit ?? policy.query.maxNodes;
57
+ const maxLimit = policy.structuralMemory?.response.maxLimit ?? policy.query.maxNodes;
58
+ return Math.max(1, Math.min(limit ?? defaultLimit, maxLimit));
59
+ }
60
+ export function safeRelativeProjectPath(projectPath, candidate) {
61
+ if (candidate.trim().length === 0 || isAbsolute(candidate) || candidate.includes('\0')) {
62
+ return null;
63
+ }
64
+ const normalized = normalize(candidate).replace(/\\/g, '/');
65
+ if (normalized === '..' || normalized.startsWith('../') || normalized.includes('/../')) {
66
+ return null;
67
+ }
68
+ const root = resolve(projectPath);
69
+ const absolute = resolve(root, normalized);
70
+ const rel = relative(root, absolute).replace(/\\/g, '/');
71
+ if (rel === '..' || rel.startsWith('../') || isAbsolute(rel)) {
72
+ return null;
73
+ }
74
+ return rel;
75
+ }
76
+ export async function loadFreshGraph(args) {
77
+ const rawFreshness = await getProjectGraphFreshness(args);
78
+ return {
79
+ graph: await readProjectGraph(args.projectId, args.policy),
80
+ freshness: freshnessStatus(rawFreshness),
81
+ };
82
+ }
83
+ export function summarizeNodes(nodes) {
84
+ return nodes.reduce((acc, node) => {
85
+ return { ...acc, [node.type]: (acc[node.type] ?? 0) + 1 };
86
+ }, {});
87
+ }
88
+ export function relatedNodeIds(nodes, edges, input) {
89
+ const seeds = new Set();
90
+ for (const node of nodes) {
91
+ const specMatch = input.specId !== undefined &&
92
+ (node.id === `spec:${input.specId}` || node.metadata?.specId === input.specId);
93
+ const fileMatch = input.filePath !== undefined &&
94
+ (node.id === `file:${input.filePath}` || node.label === input.filePath);
95
+ const nodeMatch = input.nodeId !== undefined && node.id === input.nodeId;
96
+ if (specMatch || fileMatch || nodeMatch) {
97
+ seeds.add(node.id);
98
+ }
99
+ }
100
+ if (seeds.size === 0 &&
101
+ input.specId === undefined &&
102
+ input.filePath === undefined &&
103
+ input.nodeId === undefined) {
104
+ for (const node of nodes) {
105
+ seeds.add(node.id);
106
+ }
107
+ }
108
+ const related = new Set(seeds);
109
+ for (const edge of edges) {
110
+ if (seeds.has(edge.from)) {
111
+ related.add(edge.to);
112
+ }
113
+ if (seeds.has(edge.to)) {
114
+ related.add(edge.from);
115
+ }
116
+ }
117
+ return related;
118
+ }
119
+ export async function structuralPolicy() {
120
+ return loadProjectGraphPolicy();
121
+ }
122
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1,7 @@
1
+ export type * from './contracts.js';
2
+ export { queryStructuralMemory } from './query.js';
3
+ export { detectStructuralChangeImpact } from './change-impact.js';
4
+ export { getArchitectureSnapshot } from './architecture-snapshot.js';
5
+ export { getStructuralMemoryStatus } from './status.js';
6
+ export { createStructuralMemoryArtifactPolicy, isStructuralMemoryArtifactPathAllowed, } from './artifact-policy.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ export { queryStructuralMemory } from './query.js';
2
+ export { detectStructuralChangeImpact } from './change-impact.js';
3
+ export { getArchitectureSnapshot } from './architecture-snapshot.js';
4
+ export { getStructuralMemoryStatus } from './status.js';
5
+ export { createStructuralMemoryArtifactPolicy, isStructuralMemoryArtifactPathAllowed, } from './artifact-policy.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { StructuralMemoryQueryInput, StructuralMemoryQueryResult } from './contracts.js';
2
+ export declare function queryStructuralMemory(input: StructuralMemoryQueryInput): Promise<StructuralMemoryQueryResult>;
3
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1,67 @@
1
+ import { clampLimit, loadFreshGraph, nextActionForFreshness, pagination, relatedNodeIds, resolveProjectId, safeRelativeProjectPath, structuralPolicy, summarizeNodes, } from './helpers.js';
2
+ export async function queryStructuralMemory(input) {
3
+ const policy = await structuralPolicy();
4
+ const projectId = resolveProjectId(input);
5
+ const filePath = input.filePath === undefined
6
+ ? undefined
7
+ : safeRelativeProjectPath(input.projectPath, input.filePath);
8
+ const freshnessGraph = await loadFreshGraph({
9
+ projectId,
10
+ projectPath: input.projectPath,
11
+ policy,
12
+ });
13
+ const limit = clampLimit(input.limit, policy);
14
+ const offset = Math.max(0, input.offset ?? 0);
15
+ if (input.filePath !== undefined && filePath === null) {
16
+ const page = pagination(0, limit, offset);
17
+ return {
18
+ freshness: freshnessGraph.freshness,
19
+ pagination: page,
20
+ ...page,
21
+ nextAction: {
22
+ type: 'inspect_evidence',
23
+ message: 'Rejecting unsafe filePath filter; pass a project-relative path inside the repo.',
24
+ },
25
+ nodes: [],
26
+ edges: [],
27
+ summary: {},
28
+ };
29
+ }
30
+ const graph = freshnessGraph.graph;
31
+ if (graph === null) {
32
+ const page = pagination(0, limit, offset);
33
+ return {
34
+ freshness: freshnessGraph.freshness,
35
+ pagination: page,
36
+ ...page,
37
+ nextAction: nextActionForFreshness(freshnessGraph.freshness, true),
38
+ nodes: [],
39
+ edges: [],
40
+ summary: {},
41
+ };
42
+ }
43
+ const ids = relatedNodeIds(graph.nodes, graph.edges, {
44
+ specId: input.specId,
45
+ nodeId: input.nodeId,
46
+ filePath: filePath ?? undefined,
47
+ });
48
+ const typeFilter = new Set(input.nodeTypes ?? []);
49
+ const filteredNodes = graph.nodes.filter((node) => ids.has(node.id) && (typeFilter.size === 0 || typeFilter.has(node.type)));
50
+ const nodePage = filteredNodes.slice(offset, offset + limit);
51
+ const nodeIds = new Set(nodePage.map((node) => node.id));
52
+ const edges = graph.edges.filter((edge) => nodeIds.has(edge.from) && nodeIds.has(edge.to));
53
+ const page = pagination(filteredNodes.length, limit, offset);
54
+ return {
55
+ graphVersion: graph.graphVersion,
56
+ freshness: freshnessGraph.freshness,
57
+ pagination: page,
58
+ ...page,
59
+ nextAction: nextActionForFreshness(freshnessGraph.freshness, filteredNodes.length === 0),
60
+ nodes: nodePage,
61
+ edges,
62
+ summary: summarizeNodes(nodePage),
63
+ sourceHash: graph.sourceHash,
64
+ sourceHashes: graph.sourceHashes,
65
+ };
66
+ }
67
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1,3 @@
1
+ import type { StructuralMemoryStatusInput, StructuralMemoryStatusResult } from './contracts.js';
2
+ export declare function getStructuralMemoryStatus(input: StructuralMemoryStatusInput): Promise<StructuralMemoryStatusResult>;
3
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1,30 @@
1
+ import { createStructuralMemoryArtifactPolicy, isStructuralMemoryArtifactPathAllowed, } from './artifact-policy.js';
2
+ import { loadFreshGraph, nextActionForFreshness, resolveProjectId, structuralPolicy, } from './helpers.js';
3
+ export async function getStructuralMemoryStatus(input) {
4
+ const policy = await structuralPolicy();
5
+ const projectId = resolveProjectId(input);
6
+ const { graph, freshness } = await loadFreshGraph({
7
+ projectId,
8
+ projectPath: input.projectPath,
9
+ policy,
10
+ });
11
+ const artifactPolicy = createStructuralMemoryArtifactPolicy(projectId, policy);
12
+ if (!isStructuralMemoryArtifactPathAllowed(artifactPolicy.graphPath)) {
13
+ throw new Error('Structural memory graph artifacts must remain outside planu/specs.');
14
+ }
15
+ return {
16
+ freshness,
17
+ nextAction: nextActionForFreshness(freshness, graph === null),
18
+ artifactPolicy,
19
+ graph: graph === null
20
+ ? undefined
21
+ : {
22
+ graphVersion: graph.graphVersion,
23
+ generatedAt: graph.generatedAt,
24
+ sourceHash: graph.sourceHash,
25
+ counts: graph.metadata,
26
+ sourceHashCount: Object.keys(graph.sourceHashes).length,
27
+ },
28
+ };
29
+ }
30
+ //# sourceMappingURL=status.js.map
@@ -22,6 +22,9 @@ export function withToolTracking(server) {
22
22
  // Return a wrapper function that calls the original method and captures the result
23
23
  return (...args) => {
24
24
  const name = args[0];
25
+ if (registeredTools.has(name)) {
26
+ throw new Error(`Duplicate MCP tool registration: ${name}`);
27
+ }
25
28
  const originalMethod = Reflect.get(target, prop, receiver);
26
29
  const registered = originalMethod.apply(target, args);
27
30
  registeredTools.set(name, registered);
@@ -5,6 +5,8 @@ export * from './common/index.js';
5
5
  export * from './estimation.js';
6
6
  export * from './spec/index.js';
7
7
  export * from './project.js';
8
+ export * from './project-knowledge-graph.js';
9
+ export * from './structural-memory.js';
8
10
  export * from './schema.js';
9
11
  export * from './ui.js';
10
12
  export * from './analysis.js';
@@ -6,6 +6,8 @@ export * from './common/index.js';
6
6
  export * from './estimation.js';
7
7
  export * from './spec/index.js';
8
8
  export * from './project.js';
9
+ export * from './project-knowledge-graph.js';
10
+ export * from './structural-memory.js';
9
11
  export * from './schema.js';
10
12
  export * from './ui.js';
11
13
  export * from './analysis.js';
@@ -27,6 +27,16 @@ export interface ProjectGraphPolicy {
27
27
  maxSnippetChars: number;
28
28
  redactPatterns: string[];
29
29
  };
30
+ structuralMemory?: {
31
+ schemaVersion: string;
32
+ nodeTypes: string[];
33
+ edgeTypes: string[];
34
+ response: {
35
+ defaultLimit: number;
36
+ maxLimit: number;
37
+ };
38
+ propertySchemas: Record<string, string[]>;
39
+ };
30
40
  toolNamePatterns: string[];
31
41
  }
32
42
  export interface ProjectGraphEvidencePointer {
@@ -0,0 +1,138 @@
1
+ import type { ProjectGraphEdge, ProjectGraphFreshness, ProjectGraphNode, ProjectKnowledgeGraph } from './project-knowledge-graph.js';
2
+ export type StructuralMemoryStatus = 'fresh' | 'stale' | 'missing' | 'corrupt' | 'partial';
3
+ export type StructuralMemoryNextActionType = 'none' | 'rebuild_graph' | 'broaden_filters' | 'inspect_evidence' | 'run_validate';
4
+ export interface StructuralMemoryNextAction {
5
+ type: StructuralMemoryNextActionType;
6
+ message: string;
7
+ }
8
+ export interface StructuralMemoryPagination {
9
+ total: number;
10
+ limit: number;
11
+ offset: number;
12
+ hasMore: boolean;
13
+ has_more: boolean;
14
+ truncated: boolean;
15
+ }
16
+ export interface StructuralMemoryFreshness {
17
+ status: StructuralMemoryStatus;
18
+ graphPath: string;
19
+ generatedAt?: string;
20
+ changedSources: string[];
21
+ reason: ProjectGraphFreshness['reason'] | 'partial';
22
+ }
23
+ export interface StructuralMemoryEnvelope {
24
+ graphVersion?: string;
25
+ freshness: StructuralMemoryFreshness;
26
+ pagination: StructuralMemoryPagination;
27
+ total: number;
28
+ limit: number;
29
+ offset: number;
30
+ hasMore: boolean;
31
+ has_more: boolean;
32
+ truncated: boolean;
33
+ nextAction: StructuralMemoryNextAction;
34
+ }
35
+ export interface StructuralMemoryQueryInput {
36
+ projectId?: string;
37
+ projectPath: string;
38
+ specId?: string;
39
+ filePath?: string;
40
+ nodeId?: string;
41
+ nodeTypes?: string[];
42
+ limit?: number;
43
+ offset?: number;
44
+ }
45
+ export interface StructuralMemoryQueryResult extends StructuralMemoryEnvelope {
46
+ nodes: ProjectGraphNode[];
47
+ edges: ProjectGraphEdge[];
48
+ summary: Record<string, number>;
49
+ sourceHash?: string;
50
+ sourceHashes?: Record<string, string>;
51
+ }
52
+ export type ArchitectureAspect = 'packages' | 'hotspots' | 'boundaries' | 'releaseRisks' | 'tests' | 'decisions' | 'all';
53
+ export interface ArchitectureSnapshotInput {
54
+ projectId?: string;
55
+ projectPath: string;
56
+ pathPrefix?: string;
57
+ aspects?: ArchitectureAspect[];
58
+ limit?: number;
59
+ }
60
+ export interface ArchitectureSnapshotResult extends StructuralMemoryEnvelope {
61
+ aspects: {
62
+ packages?: {
63
+ name: string;
64
+ fileCount: number;
65
+ }[];
66
+ hotspots?: {
67
+ id: string;
68
+ label: string;
69
+ type: string;
70
+ degree: number;
71
+ }[];
72
+ boundaries?: {
73
+ from: string;
74
+ to: string;
75
+ type: string;
76
+ }[];
77
+ releaseRisks?: {
78
+ id: string;
79
+ label: string;
80
+ type: string;
81
+ }[];
82
+ tests?: {
83
+ id: string;
84
+ label: string;
85
+ }[];
86
+ decisions?: {
87
+ id: string;
88
+ label: string;
89
+ }[];
90
+ };
91
+ }
92
+ export interface StructuralMemoryStatusInput {
93
+ projectId?: string;
94
+ projectPath: string;
95
+ }
96
+ export interface StructuralMemoryArtifactPolicy {
97
+ artifactRoot: string;
98
+ graphPath: string;
99
+ projectDataDirectory: string;
100
+ forbiddenPathFragment: 'planu/specs';
101
+ specDirectoryPolicy: 'spec.md-only';
102
+ evidenceAndGraphArtifactsRemainOutsideSpecs: true;
103
+ }
104
+ export interface StructuralMemoryStatusResult {
105
+ freshness: StructuralMemoryFreshness;
106
+ nextAction: StructuralMemoryNextAction;
107
+ artifactPolicy: StructuralMemoryArtifactPolicy;
108
+ graph?: Pick<ProjectKnowledgeGraph, 'graphVersion' | 'generatedAt' | 'sourceHash'> & {
109
+ counts: ProjectKnowledgeGraph['metadata'];
110
+ sourceHashCount: number;
111
+ };
112
+ }
113
+ export interface StructuralChangeImpactInput {
114
+ projectId?: string;
115
+ projectPath: string;
116
+ baseRef?: string;
117
+ changedFiles?: string[];
118
+ includeUntracked?: boolean;
119
+ limit?: number;
120
+ }
121
+ export interface StructuralChangedFile {
122
+ path: string;
123
+ status: 'added' | 'modified' | 'deleted' | 'renamed' | 'untracked' | 'unknown';
124
+ }
125
+ export interface StructuralChangeImpactResult extends StructuralMemoryEnvelope {
126
+ changedFiles: StructuralChangedFile[];
127
+ affected: {
128
+ specs: ProjectGraphNode[];
129
+ criteria: ProjectGraphNode[];
130
+ tests: ProjectGraphNode[];
131
+ decisions: ProjectGraphNode[];
132
+ evidence: ProjectGraphNode[];
133
+ risks: ProjectGraphNode[];
134
+ releaseRisk: ProjectGraphNode[];
135
+ };
136
+ affectedEdges: ProjectGraphEdge[];
137
+ }
138
+ //# sourceMappingURL=structural-memory.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=structural-memory.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.7.5",
3
+ "version": "4.9.0",
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",
@@ -34,14 +34,14 @@
34
34
  "packageName": "@planu/core"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@planu/core-darwin-arm64": "4.7.5",
38
- "@planu/core-darwin-x64": "4.7.5",
39
- "@planu/core-linux-arm64-gnu": "4.7.5",
40
- "@planu/core-linux-arm64-musl": "4.7.5",
41
- "@planu/core-linux-x64-gnu": "4.7.5",
42
- "@planu/core-linux-x64-musl": "4.7.5",
43
- "@planu/core-win32-arm64-msvc": "4.7.5",
44
- "@planu/core-win32-x64-msvc": "4.7.5"
37
+ "@planu/core-darwin-arm64": "4.9.0",
38
+ "@planu/core-darwin-x64": "4.9.0",
39
+ "@planu/core-linux-arm64-gnu": "4.9.0",
40
+ "@planu/core-linux-arm64-musl": "4.9.0",
41
+ "@planu/core-linux-x64-gnu": "4.9.0",
42
+ "@planu/core-linux-x64-musl": "4.9.0",
43
+ "@planu/core-win32-arm64-msvc": "4.9.0",
44
+ "@planu/core-win32-x64-msvc": "4.9.0"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24.0.0"
@@ -129,7 +129,7 @@
129
129
  ],
130
130
  "license": "SEE LICENSE IN LICENSE",
131
131
  "dependencies": {
132
- "@anthropic-ai/sdk": "^0.105.0",
132
+ "@anthropic-ai/sdk": "^0.107.0",
133
133
  "@modelcontextprotocol/sdk": "^1.29.0",
134
134
  "glob": "^13.0.6",
135
135
  "yaml": "^2.9.0",
@@ -168,8 +168,8 @@
168
168
  }
169
169
  },
170
170
  "devDependencies": {
171
- "@commitlint/cli": "^21.1.0",
172
- "@commitlint/config-conventional": "^21.1.0",
171
+ "@commitlint/cli": "^21.2.0",
172
+ "@commitlint/config-conventional": "^21.2.0",
173
173
  "@eslint/js": "^10.0.1",
174
174
  "@napi-rs/cli": "^3.7.2",
175
175
  "@secretlint/secretlint-rule-no-homedir": "^13.0.2",
@@ -182,30 +182,30 @@
182
182
  "@semantic-release/release-notes-generator": "^14.1.1",
183
183
  "@stryker-mutator/core": "^9.6.1",
184
184
  "@stryker-mutator/vitest-runner": "^9.6.1",
185
- "@supabase/supabase-js": "^2.108.2",
186
- "@types/node": "^26.0.0",
185
+ "@supabase/supabase-js": "^2.110.0",
186
+ "@types/node": "^26.0.1",
187
187
  "@vitejs/plugin-vue": "^6.0.7",
188
188
  "@vitest/coverage-v8": "^4.1.9",
189
189
  "@vue/test-utils": "^2.4.11",
190
- "eslint": "^10.5.0",
190
+ "eslint": "^10.6.0",
191
191
  "eslint-config-prettier": "^10.1.8",
192
192
  "eslint-import-resolver-typescript": "^4.4.5",
193
193
  "eslint-plugin-import": "^2.32.0",
194
194
  "happy-dom": "^20.10.6",
195
195
  "husky": "^9.1.7",
196
196
  "javascript-obfuscator": "^5.4.3",
197
- "knip": "^6.20.0",
197
+ "knip": "^6.23.0",
198
198
  "lint-staged": "^17.0.8",
199
199
  "madge": "^8.0.0",
200
- "prettier": "^3.8.4",
200
+ "prettier": "^3.9.4",
201
201
  "secretlint": "^13.0.2",
202
202
  "semantic-release": "^25.0.5",
203
203
  "tsc-alias": "^1.8.17",
204
204
  "type-coverage": "^2.29.7",
205
205
  "typescript": "^6.0.3",
206
- "typescript-eslint": "^8.62.0",
207
- "vite": "^8.1.0",
206
+ "typescript-eslint": "^8.62.1",
207
+ "vite": "^8.1.1",
208
208
  "vitest": "^4.1.9",
209
- "vue": "^3.5.38"
209
+ "vue": "^3.5.39"
210
210
  }
211
211
  }
package/planu-native.json CHANGED
@@ -1,20 +1,26 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "4.7.5",
4
+ "version": "4.9.0",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
8
8
  "requiresMcp": false,
9
9
  "requiresDaemon": false,
10
- "hosts": ["codex", "claude-code"],
10
+ "hosts": [
11
+ "codex",
12
+ "claude-code"
13
+ ],
11
14
  "commands": [
12
15
  {
13
16
  "id": "planu.status",
14
17
  "title": "Project status",
15
18
  "description": "Show the compact Planu project snapshot without loading the MCP tool graph.",
16
19
  "invocation": "planu status",
17
- "hosts": ["codex", "claude-code"],
20
+ "hosts": [
21
+ "codex",
22
+ "claude-code"
23
+ ],
18
24
  "requiresMcp": false,
19
25
  "requiresDaemon": false,
20
26
  "mapsTo": "handlePlanStatus"
@@ -24,7 +30,10 @@
24
30
  "title": "Create spec",
25
31
  "description": "Create a new spec through the CLI-backed SDD contract.",
26
32
  "invocation": "planu spec create \"<title>\"",
27
- "hosts": ["codex", "claude-code"],
33
+ "hosts": [
34
+ "codex",
35
+ "claude-code"
36
+ ],
28
37
  "requiresMcp": false,
29
38
  "requiresDaemon": false,
30
39
  "mapsTo": "handleCreateSpec"
@@ -34,7 +43,10 @@
34
43
  "title": "List specs",
35
44
  "description": "List specs in the current project with optional status/type filters.",
36
45
  "invocation": "planu spec list",
37
- "hosts": ["codex", "claude-code"],
46
+ "hosts": [
47
+ "codex",
48
+ "claude-code"
49
+ ],
38
50
  "requiresMcp": false,
39
51
  "requiresDaemon": false,
40
52
  "mapsTo": "handleListSpecs"
@@ -44,7 +56,10 @@
44
56
  "title": "Validate spec",
45
57
  "description": "Validate a spec against the current codebase from the native CLI surface.",
46
58
  "invocation": "planu spec validate SPEC-001",
47
- "hosts": ["codex", "claude-code"],
59
+ "hosts": [
60
+ "codex",
61
+ "claude-code"
62
+ ],
48
63
  "requiresMcp": false,
49
64
  "requiresDaemon": false,
50
65
  "mapsTo": "handleValidate"
@@ -54,7 +69,10 @@
54
69
  "title": "Audit technical debt",
55
70
  "description": "Run the read-only project audit path for lightweight debt checks.",
56
71
  "invocation": "planu audit debt",
57
- "hosts": ["codex", "claude-code"],
72
+ "hosts": [
73
+ "codex",
74
+ "claude-code"
75
+ ],
58
76
  "requiresMcp": false,
59
77
  "requiresDaemon": false,
60
78
  "mapsTo": "handleAudit"
@@ -64,7 +82,10 @@
64
82
  "title": "Check release readiness",
65
83
  "description": "Check local-first release readiness, branch cleanliness, and optional gitflow drift.",
66
84
  "invocation": "planu release check",
67
- "hosts": ["codex", "claude-code"],
85
+ "hosts": [
86
+ "codex",
87
+ "claude-code"
88
+ ],
68
89
  "requiresMcp": false,
69
90
  "requiresDaemon": false,
70
91
  "mapsTo": "releaseCommand"
package/planu-plugin.json CHANGED
@@ -2,9 +2,12 @@
2
2
  "name": "dev.planu.cli",
3
3
  "displayName": "Planu — Spec Driven Development",
4
4
  "description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
5
- "version": "4.7.5",
5
+ "version": "4.9.0",
6
6
  "icon": "assets/plugin/icon.svg",
7
- "command": ["npx", "@planu/cli@latest"],
7
+ "command": [
8
+ "npx",
9
+ "@planu/cli@latest"
10
+ ],
8
11
  "packageName": "@planu/cli",
9
12
  "capabilities": {
10
13
  "tools": [
@@ -23,17 +26,42 @@
23
26
  "create_skill",
24
27
  "skill_search"
25
28
  ],
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
+ "resources": [
30
+ "planu://specs/list",
31
+ "planu://specs/{id}",
32
+ "planu://project/status",
33
+ "planu://roadmap"
34
+ ],
35
+ "prompts": [
36
+ "create-spec-from-idea",
37
+ "review-spec-readiness",
38
+ "generate-implementation-plan"
39
+ ],
40
+ "subagents": [
41
+ "sdd-orchestrator",
42
+ "spec-challenger",
43
+ "test-generator"
44
+ ]
29
45
  },
30
46
  "compatibility": {
31
47
  "minimumHostVersion": "1.0.0",
32
- "requiredFeatures": ["mcp-tools", "file-editing"]
48
+ "requiredFeatures": [
49
+ "mcp-tools",
50
+ "file-editing"
51
+ ]
33
52
  },
34
53
  "repository": "https://github.com/planu-dev/planu",
35
54
  "author": "Planu",
36
55
  "license": "MIT",
37
56
  "homepage": "https://planu.dev",
38
- "keywords": ["sdd", "spec-driven-development", "mcp", "specs", "planning", "ai", "bdd", "tdd"]
57
+ "keywords": [
58
+ "sdd",
59
+ "spec-driven-development",
60
+ "mcp",
61
+ "specs",
62
+ "planning",
63
+ "ai",
64
+ "bdd",
65
+ "tdd"
66
+ ]
39
67
  }