@planu/cli 4.1.1 → 4.1.3

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 (62) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/config/license-plans.json +65 -361
  3. package/dist/engine/core-bridge.js +35 -4
  4. package/dist/engine/hooks/file-watcher.d.ts +6 -0
  5. package/dist/engine/hooks/file-watcher.js +69 -16
  6. package/dist/tools/git/hook-ops.js +23 -9
  7. package/dist/tools/tool-registry/group-infra.js +22 -0
  8. package/package.json +7 -7
  9. package/dist/engine/escalator/index.d.ts +0 -5
  10. package/dist/engine/escalator/index.js +0 -5
  11. package/dist/engine/freeze/retro-audit.d.ts +0 -6
  12. package/dist/engine/freeze/retro-audit.js +0 -24
  13. package/dist/engine/heal/backup.d.ts +0 -9
  14. package/dist/engine/heal/backup.js +0 -21
  15. package/dist/engine/idioma-validator/index.d.ts +0 -17
  16. package/dist/engine/idioma-validator/index.js +0 -89
  17. package/dist/engine/saga/index.d.ts +0 -4
  18. package/dist/engine/saga/index.js +0 -4
  19. package/dist/engine/spec-state-machine/index.d.ts +0 -3
  20. package/dist/engine/spec-state-machine/index.js +0 -2
  21. package/dist/engine/spec-summary-html/dashboard-renderer.d.ts +0 -6
  22. package/dist/engine/spec-summary-html/dashboard-renderer.js +0 -333
  23. package/dist/engine/triagier/index.d.ts +0 -5
  24. package/dist/engine/triagier/index.js +0 -5
  25. package/dist/engine/universal-rules/index.d.ts +0 -5
  26. package/dist/engine/universal-rules/index.js +0 -6
  27. package/dist/testing/cassette/index.d.ts +0 -23
  28. package/dist/testing/cassette/index.js +0 -26
  29. package/dist/tools/domain-bundle-handler.d.ts +0 -37
  30. package/dist/tools/domain-bundle-handler.js +0 -71
  31. package/dist/tools/figma/rules-file.d.ts +0 -5
  32. package/dist/tools/figma/rules-file.js +0 -45
  33. package/dist/tools/heal-planu-root.d.ts +0 -8
  34. package/dist/tools/heal-planu-root.js +0 -144
  35. package/dist/tools/opencode-host-adapter.d.ts +0 -3
  36. package/dist/tools/opencode-host-adapter.js +0 -33
  37. package/dist/tools/plan-team-distribution.d.ts +0 -3
  38. package/dist/tools/plan-team-distribution.js +0 -71
  39. package/dist/tools/reconcile-status-json.d.ts +0 -4
  40. package/dist/tools/reconcile-status-json.js +0 -209
  41. package/dist/tools/register-all-tools.d.ts +0 -8
  42. package/dist/tools/register-all-tools.js +0 -239
  43. package/dist/tools/tool-registry/group-analysis-monitoring.d.ts +0 -3
  44. package/dist/tools/tool-registry/group-analysis-monitoring.js +0 -942
  45. package/dist/tools/tool-registry/group-integrations.d.ts +0 -3
  46. package/dist/tools/tool-registry/group-integrations.js +0 -1046
  47. package/dist/tools/tool-registry/group-misc.d.ts +0 -3
  48. package/dist/tools/tool-registry/group-misc.js +0 -1367
  49. package/dist/tools/tool-registry/group-platform.d.ts +0 -3
  50. package/dist/tools/tool-registry/group-platform.js +0 -1681
  51. package/dist/tools/tool-registry/group-session-knowledge.d.ts +0 -3
  52. package/dist/tools/tool-registry/group-session-knowledge.js +0 -1416
  53. package/dist/tools/tool-registry/group-spec-ops.d.ts +0 -3
  54. package/dist/tools/tool-registry/group-spec-ops.js +0 -917
  55. package/dist/tools/workspace-overview.d.ts +0 -4
  56. package/dist/tools/workspace-overview.js +0 -316
  57. package/dist/transports/middleware/index.d.ts +0 -9
  58. package/dist/transports/middleware/index.js +0 -7
  59. package/dist/transports/middleware/with-sandbox.d.ts +0 -21
  60. package/dist/transports/middleware/with-sandbox.js +0 -68
  61. package/dist/types/heal.d.ts +0 -18
  62. package/dist/types/heal.js +0 -3
@@ -1,144 +0,0 @@
1
- // tools/heal-planu-root.ts — SPEC-745
2
- // heal_planu_root tool with 3-tier policy: auto-fix / propose-only / never-touch
3
- import { z } from 'zod';
4
- import { readdirSync, existsSync } from 'node:fs';
5
- import { join } from 'node:path';
6
- import { safeTracked } from './safe-handler.js';
7
- import { readDoNotHealList } from '../engine/heal/markers.js';
8
- import { classifyFile } from '../engine/heal/policy.js';
9
- import { backupFile } from '../engine/heal/backup.js';
10
- import { atomicWriteFile } from '../engine/safety/atomic-write-file.js';
11
- import { writeHealProposal } from '../engine/heal/proposal-writer.js';
12
- // ---------------------------------------------------------------------------
13
- // Public API
14
- // ---------------------------------------------------------------------------
15
- export async function healPlanuRoot(opts) {
16
- const { projectRoot, dryRun = false } = opts;
17
- const planuDir = join(projectRoot, 'planu');
18
- const allowlist = readDoNotHealList(projectRoot);
19
- // Collect candidate files from planu/ root (non-recursive for safety)
20
- const candidateFiles = [];
21
- if (existsSync(planuDir)) {
22
- try {
23
- const entries = readdirSync(planuDir, { withFileTypes: true });
24
- for (const e of entries) {
25
- if (e.isFile() && (e.name.endsWith('.json') || e.name.endsWith('.jsonl'))) {
26
- candidateFiles.push(join(planuDir, e.name));
27
- }
28
- }
29
- }
30
- catch {
31
- // ignore
32
- }
33
- }
34
- const results = [];
35
- const skipped = [];
36
- const now = Date.now();
37
- for (const filePath of candidateFiles) {
38
- const policy = classifyFile(filePath, allowlist);
39
- if (policy.tier === 3) {
40
- if (policy.reason === 'clean') {
41
- continue; // not a candidate
42
- }
43
- skipped.push({ path: filePath, reason: policy.reason });
44
- results.push({ path: filePath, tier: 3, action: 'skipped', reason: policy.reason });
45
- continue;
46
- }
47
- if (policy.tier === 1 && policy.repairedContent !== undefined) {
48
- if (!dryRun) {
49
- // Backup BEFORE write
50
- const backupPath = await backupFile(filePath, now);
51
- await atomicWriteFile(filePath, policy.repairedContent);
52
- results.push({
53
- path: filePath,
54
- tier: 1,
55
- action: 'auto-fixed',
56
- backup: backupPath,
57
- reason: policy.reason,
58
- });
59
- }
60
- else {
61
- results.push({
62
- path: filePath,
63
- tier: 1,
64
- action: 'auto-fixed',
65
- reason: `[dryRun] ${policy.reason}`,
66
- });
67
- }
68
- continue;
69
- }
70
- if (policy.tier === 2 && policy.proposedContent !== undefined) {
71
- if (!dryRun) {
72
- const proposalPath = await writeHealProposal(filePath, {
73
- originalPath: filePath,
74
- proposedContent: policy.proposedContent,
75
- violations: policy.violations ?? [],
76
- generatedAt: new Date().toISOString(),
77
- }, now);
78
- results.push({
79
- path: filePath,
80
- tier: 2,
81
- action: 'proposal-emitted',
82
- proposalPath,
83
- reason: policy.reason,
84
- });
85
- }
86
- else {
87
- results.push({
88
- path: filePath,
89
- tier: 2,
90
- action: 'proposal-emitted',
91
- reason: `[dryRun] ${policy.reason}`,
92
- });
93
- }
94
- }
95
- }
96
- return { scanned: candidateFiles.length, results, skipped };
97
- }
98
- // ---------------------------------------------------------------------------
99
- // MCP registration
100
- // ---------------------------------------------------------------------------
101
- export function registerHealPlanuRootTool(server) {
102
- server.registerTool('heal_planu_root', {
103
- description: 'Run a 3-tier heal pass over planu/ config files. ' +
104
- 'Tier-1 (auto-fix): invalid JSON syntax is repaired atomically with a .bak backup. ' +
105
- 'Tier-2 (propose-only): out-of-range values generate a .heal-proposal.<ts>.json for human review. ' +
106
- 'Tier-3 (never-touch): files with `// custom` header or in .planu-do-not-heal are skipped. ' +
107
- 'Use dryRun=true to preview without writing.',
108
- inputSchema: {
109
- projectPath: z.string().min(1).max(4096).describe('Absolute path to project root.'),
110
- dryRun: z
111
- .boolean()
112
- .optional()
113
- .describe('Preview mode — reports what would be done without writing. Default: false.'),
114
- },
115
- }, safeTracked('heal_planu_root', async (args) => {
116
- const { projectPath, dryRun } = args;
117
- const result = await healPlanuRoot({ projectRoot: projectPath, dryRun });
118
- const autoFixed = result.results.filter((r) => r.action === 'auto-fixed').length;
119
- const proposals = result.results.filter((r) => r.action === 'proposal-emitted').length;
120
- const skippedCount = result.results.filter((r) => r.action === 'skipped').length;
121
- return {
122
- content: [
123
- {
124
- type: 'text',
125
- text: [
126
- `# Heal Planu Root${dryRun ? ' (dry run)' : ''}`,
127
- ``,
128
- `**Scanned**: ${result.scanned} files`,
129
- `**Auto-fixed (tier-1)**: ${autoFixed}`,
130
- `**Proposals emitted (tier-2)**: ${proposals}`,
131
- `**Skipped (tier-3)**: ${skippedCount}`,
132
- ``,
133
- result.results.length > 0
134
- ? result.results
135
- .map((r) => `- [tier-${r.tier}] ${r.action}: \`${r.path}\`${r.backup ? ` → backup: \`${r.backup}\`` : ''}${r.proposalPath ? ` → proposal: \`${r.proposalPath}\`` : ''}${r.reason ? ` (${r.reason})` : ''}`)
136
- .join('\n')
137
- : '_No issues found._',
138
- ].join('\n'),
139
- },
140
- ],
141
- };
142
- }));
143
- }
144
- //# sourceMappingURL=heal-planu-root.js.map
@@ -1,3 +0,0 @@
1
- import type { OpenCodeAdapterInput, ToolResult } from '../types/index.js';
2
- export declare function handleOpenCodeHostAdapter(args: OpenCodeAdapterInput): Promise<ToolResult>;
3
- //# sourceMappingURL=opencode-host-adapter.d.ts.map
@@ -1,33 +0,0 @@
1
- // src/tools/opencode-host-adapter.ts — SPEC-966 tool handler
2
- import { runOpenCodeAdapter } from '../engine/opencode/adapter.js';
3
- export async function handleOpenCodeHostAdapter(args) {
4
- const result = await runOpenCodeAdapter(args);
5
- const lines = [];
6
- lines.push(`# OpenCode Host Adapter — ${result.detected ? '✅ Detected' : '❌ Not Detected'}`);
7
- lines.push('');
8
- lines.push(`**Markers found:** ${result.markers.join(', ') || 'none'}`);
9
- if (result.configPath) {
10
- lines.push(`**Config path:** ${result.configPath}`);
11
- }
12
- lines.push('');
13
- if (result.scaffoldedFiles) {
14
- lines.push('## Scaffolded Files');
15
- for (const file of result.scaffoldedFiles) {
16
- lines.push(`- ${file}`);
17
- }
18
- lines.push('');
19
- }
20
- if (result.coachRules) {
21
- lines.push('## Coach Rules');
22
- for (const rule of result.coachRules) {
23
- lines.push(`- ${rule}`);
24
- }
25
- lines.push('');
26
- }
27
- return {
28
- content: [{ type: 'text', text: lines.join('\n') }],
29
- structuredContent: result,
30
- isError: false,
31
- };
32
- }
33
- //# sourceMappingURL=opencode-host-adapter.js.map
@@ -1,3 +0,0 @@
1
- import type { PlanTeamDistributionInput, ToolResult } from '../types/index.js';
2
- export declare function handlePlanTeamDistribution(params: PlanTeamDistributionInput): Promise<ToolResult>;
3
- //# sourceMappingURL=plan-team-distribution.d.ts.map
@@ -1,71 +0,0 @@
1
- // tools/plan-team-distribution.ts — Expose planTeamDistribution as MCP tool (SPEC-091)
2
- // Loads specs, extracts file ownership from each spec.md, and delegates to the engine.
3
- import { readFile } from 'node:fs/promises';
4
- import { specStore } from '../storage/index.js';
5
- import { planTeamDistribution } from '../engine/team-planner/index.js';
6
- const FILE_PATTERN = /\b([\w./-]+\.(?:ts|tsx|js|jsx|py|go|rs|java|rb|cs|json|yaml|yml|vue|svelte))\b/g;
7
- function extractFilesFromContent(content) {
8
- const found = new Set();
9
- for (const line of content.split('\n')) {
10
- if (line.trimStart().startsWith('#')) {
11
- continue;
12
- }
13
- FILE_PATTERN.lastIndex = 0;
14
- let match;
15
- while ((match = FILE_PATTERN.exec(line)) !== null) {
16
- const raw = match[1];
17
- if (raw && raw.length >= 5) {
18
- found.add(raw);
19
- }
20
- }
21
- }
22
- return [...found];
23
- }
24
- async function readSpecFiles(specPath) {
25
- try {
26
- const content = await readFile(specPath, 'utf-8');
27
- return extractFilesFromContent(content);
28
- }
29
- catch {
30
- return [];
31
- }
32
- }
33
- export async function handlePlanTeamDistribution(params) {
34
- const { projectId, specIds, template } = params;
35
- if (specIds.length === 0) {
36
- return {
37
- content: [
38
- {
39
- type: 'text',
40
- text: 'specIds must contain at least one spec ID. Provide the specs to distribute across the team.',
41
- },
42
- ],
43
- isError: true,
44
- };
45
- }
46
- const specs = [];
47
- const fileOwnership = {};
48
- for (const specId of specIds) {
49
- const spec = await specStore.getSpec(projectId, specId);
50
- if (!spec) {
51
- return {
52
- content: [
53
- {
54
- type: 'text',
55
- text: `Spec '${specId}' not found in project '${projectId}'. Check the spec ID.`,
56
- },
57
- ],
58
- isError: true,
59
- };
60
- }
61
- specs.push({ id: spec.id, title: spec.title });
62
- fileOwnership[spec.id] = await readSpecFiles(spec.specPath);
63
- }
64
- const templateRef = template !== undefined && template.length > 0 ? { id: template, name: template } : undefined;
65
- const plan = planTeamDistribution(specs, fileOwnership, templateRef);
66
- return {
67
- content: [{ type: 'text', text: JSON.stringify(plan, null, 2) }],
68
- structuredContent: { plan },
69
- };
70
- }
71
- //# sourceMappingURL=plan-team-distribution.js.map
@@ -1,4 +0,0 @@
1
- import type { ToolResult } from '../types/index.js';
2
- import type { ReconcileStatusJsonInput } from '../types/workspace-overview.js';
3
- export declare function handleReconcileStatusJson(params: ReconcileStatusJsonInput): Promise<ToolResult>;
4
- //# sourceMappingURL=reconcile-status-json.d.ts.map
@@ -1,209 +0,0 @@
1
- // tools/reconcile-status-json.ts — SPEC-753: reconcile_status_json tool
2
- //
3
- // Reconciles status.json spec statuses with actual spec.md frontmatter values.
4
- // Source-of-truth rule: 'frontmatter-wins' | 'status-wins' | 'newest-wins'.
5
- import { join } from 'node:path';
6
- import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises';
7
- import { resolveProjectPath } from '../storage/path-resolver.js';
8
- import { hashProjectPath } from '../storage/base-store.js';
9
- import { withStatusLock } from '../storage/status-store/file-lock.js';
10
- // ---------------------------------------------------------------------------
11
- // Frontmatter parser helpers (local, not exported — types in types/workspace-overview.ts)
12
- // ---------------------------------------------------------------------------
13
- function parseFrontmatter(content) {
14
- const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
15
- if (!match?.[1]) {
16
- return null;
17
- }
18
- const block = match[1];
19
- const result = {};
20
- for (const line of block.split('\n')) {
21
- const colonIdx = line.indexOf(':');
22
- if (colonIdx === -1) {
23
- continue;
24
- }
25
- const key = line.slice(0, colonIdx).trim();
26
- const val = line
27
- .slice(colonIdx + 1)
28
- .trim()
29
- .replace(/^["']|["']$/g, '');
30
- if (key) {
31
- result[key] = val;
32
- }
33
- }
34
- if (!result.id || !result.status) {
35
- return null;
36
- }
37
- return {
38
- id: result.id,
39
- status: result.status,
40
- updated: result.updated,
41
- };
42
- }
43
- async function readStatusJson(statusPath) {
44
- try {
45
- const raw = await readFile(statusPath, 'utf-8');
46
- const parsed = JSON.parse(raw);
47
- if (typeof parsed === 'object' && parsed !== null) {
48
- return parsed;
49
- }
50
- return null;
51
- }
52
- catch {
53
- return null;
54
- }
55
- }
56
- /**
57
- * Build a map of specId → status from recentChanges in status.json.
58
- */
59
- function buildStatusJsonSpecMap(content) {
60
- const map = new Map();
61
- if (Array.isArray(content.recentChanges)) {
62
- for (const change of content.recentChanges) {
63
- if (change.specId && change.to) {
64
- map.set(change.specId, change.to);
65
- }
66
- }
67
- }
68
- return map;
69
- }
70
- function resolveConflict(_specId, statusJsonStatus, frontmatterStatus, _statusJsonUpdatedAt, source) {
71
- const sjStatus = statusJsonStatus ?? 'unknown';
72
- if (sjStatus === frontmatterStatus) {
73
- return { resolved: frontmatterStatus, changed: false };
74
- }
75
- let resolved;
76
- if (source === 'frontmatter-wins') {
77
- resolved = frontmatterStatus;
78
- }
79
- else if (source === 'status-wins') {
80
- resolved = sjStatus !== 'unknown' ? sjStatus : frontmatterStatus;
81
- }
82
- else {
83
- // newest-wins: frontmatter is considered most recent edit
84
- resolved = frontmatterStatus;
85
- }
86
- return { resolved, changed: true };
87
- }
88
- // ---------------------------------------------------------------------------
89
- // Main handler
90
- // ---------------------------------------------------------------------------
91
- export async function handleReconcileStatusJson(params) {
92
- const { source = 'frontmatter-wins', dryRun = true } = params;
93
- const { projectPath } = await resolveProjectPath(params.projectPath);
94
- const projectId = hashProjectPath(projectPath);
95
- const specsDir = join(projectPath, 'planu', 'specs');
96
- const statusPath = join(projectPath, 'planu', 'status.json');
97
- const statusContent = await readStatusJson(statusPath);
98
- const statusJsonMap = statusContent
99
- ? buildStatusJsonSpecMap(statusContent)
100
- : new Map();
101
- const statusJsonUpdatedAt = statusContent?.updatedAt;
102
- const conflicts = [];
103
- let specDirs = [];
104
- try {
105
- specDirs = await readdir(specsDir);
106
- }
107
- catch {
108
- // No specs dir
109
- }
110
- for (const dir of specDirs) {
111
- const specPath = join(specsDir, dir, 'spec.md');
112
- let content;
113
- try {
114
- content = await readFile(specPath, 'utf-8');
115
- }
116
- catch {
117
- continue;
118
- }
119
- const fm = parseFrontmatter(content);
120
- if (!fm) {
121
- continue;
122
- }
123
- const sjStatus = statusJsonMap.get(fm.id);
124
- const { resolved, changed } = resolveConflict(fm.id, sjStatus, fm.status, statusJsonUpdatedAt, source);
125
- if (changed) {
126
- conflicts.push({
127
- specId: fm.id,
128
- statusJson: sjStatus ?? 'not-in-recentChanges',
129
- frontmatter: fm.status,
130
- resolved,
131
- });
132
- }
133
- }
134
- if (!dryRun && conflicts.length > 0 && statusContent) {
135
- await withStatusLock(statusPath, async () => {
136
- const { specStore } = await import('../storage/index.js');
137
- const allSpecs = await specStore.listSpecs(projectId);
138
- const correctedMap = new Map();
139
- for (const c of conflicts) {
140
- correctedMap.set(c.specId, c.resolved);
141
- }
142
- const newByStatus = {};
143
- for (const spec of allSpecs) {
144
- const correctedStatus = correctedMap.get(spec.id) ?? spec.status;
145
- newByStatus[correctedStatus] = (newByStatus[correctedStatus] ?? 0) + 1;
146
- }
147
- const updated = {
148
- ...statusContent,
149
- byStatus: newByStatus,
150
- totalSpecs: allSpecs.length,
151
- updatedAt: new Date().toISOString(),
152
- };
153
- await mkdir(join(projectPath, 'planu'), { recursive: true });
154
- await writeFile(statusPath, JSON.stringify(updated, null, 2), 'utf-8');
155
- });
156
- // Log corrections to transition-log.jsonl
157
- try {
158
- const { appendTransitionEvent } = await import('../storage/transition-log.js');
159
- for (const conflict of conflicts) {
160
- await appendTransitionEvent({
161
- projectId,
162
- specId: conflict.specId,
163
- eventType: 'status_reconciled',
164
- from: conflict.statusJson,
165
- to: conflict.resolved,
166
- actor: 'reconcile_status_json',
167
- reason: `source: ${source}`,
168
- sessionId: 'reconcile',
169
- });
170
- }
171
- }
172
- catch {
173
- // Non-fatal
174
- }
175
- }
176
- const result = {
177
- corrected: conflicts.length,
178
- dryRun,
179
- source,
180
- conflicts,
181
- message: dryRun
182
- ? `Dry run: found ${String(conflicts.length)} discrepancies. Set dryRun=false to apply.`
183
- : `Applied ${String(conflicts.length)} corrections with source '${source}'.`,
184
- };
185
- const lines = [
186
- `# reconcile_status_json${dryRun ? ' (dry run)' : ''}`,
187
- ``,
188
- `**Source:** ${source}`,
189
- `**Discrepancies found:** ${String(conflicts.length)}`,
190
- `**Applied:** ${dryRun ? 'No (dryRun=true)' : 'Yes'}`,
191
- ``,
192
- ];
193
- if (conflicts.length > 0) {
194
- lines.push(`## Conflicts`);
195
- lines.push(`| Spec | status.json | frontmatter | resolved |`);
196
- lines.push(`|------|------------|-------------|----------|`);
197
- for (const c of conflicts) {
198
- lines.push(`| ${c.specId} | ${c.statusJson} | ${c.frontmatter} | **${c.resolved}** |`);
199
- }
200
- }
201
- else {
202
- lines.push(`No discrepancies found — status.json is in sync with frontmatters.`);
203
- }
204
- return {
205
- content: [{ type: 'text', text: lines.join('\n') }],
206
- structuredContent: result,
207
- };
208
- }
209
- //# sourceMappingURL=reconcile-status-json.js.map
@@ -1,8 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- /**
3
- * Registers all MCP tools onto the server instance.
4
- * Previously split across registerCoreTools / registerExtendedTools /
5
- * registerLatestSpecTools / registerIntegrationTools in index.ts.
6
- */
7
- export declare function registerAllTools(s: McpServer): void;
8
- //# sourceMappingURL=register-all-tools.d.ts.map