@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.
- package/CHANGELOG.md +21 -0
- package/dist/config/license-plans.json +65 -361
- package/dist/engine/core-bridge.js +35 -4
- package/dist/engine/hooks/file-watcher.d.ts +6 -0
- package/dist/engine/hooks/file-watcher.js +69 -16
- package/dist/tools/git/hook-ops.js +23 -9
- package/dist/tools/tool-registry/group-infra.js +22 -0
- package/package.json +7 -7
- package/dist/engine/escalator/index.d.ts +0 -5
- package/dist/engine/escalator/index.js +0 -5
- package/dist/engine/freeze/retro-audit.d.ts +0 -6
- package/dist/engine/freeze/retro-audit.js +0 -24
- package/dist/engine/heal/backup.d.ts +0 -9
- package/dist/engine/heal/backup.js +0 -21
- package/dist/engine/idioma-validator/index.d.ts +0 -17
- package/dist/engine/idioma-validator/index.js +0 -89
- package/dist/engine/saga/index.d.ts +0 -4
- package/dist/engine/saga/index.js +0 -4
- package/dist/engine/spec-state-machine/index.d.ts +0 -3
- package/dist/engine/spec-state-machine/index.js +0 -2
- package/dist/engine/spec-summary-html/dashboard-renderer.d.ts +0 -6
- package/dist/engine/spec-summary-html/dashboard-renderer.js +0 -333
- package/dist/engine/triagier/index.d.ts +0 -5
- package/dist/engine/triagier/index.js +0 -5
- package/dist/engine/universal-rules/index.d.ts +0 -5
- package/dist/engine/universal-rules/index.js +0 -6
- package/dist/testing/cassette/index.d.ts +0 -23
- package/dist/testing/cassette/index.js +0 -26
- package/dist/tools/domain-bundle-handler.d.ts +0 -37
- package/dist/tools/domain-bundle-handler.js +0 -71
- package/dist/tools/figma/rules-file.d.ts +0 -5
- package/dist/tools/figma/rules-file.js +0 -45
- package/dist/tools/heal-planu-root.d.ts +0 -8
- package/dist/tools/heal-planu-root.js +0 -144
- package/dist/tools/opencode-host-adapter.d.ts +0 -3
- package/dist/tools/opencode-host-adapter.js +0 -33
- package/dist/tools/plan-team-distribution.d.ts +0 -3
- package/dist/tools/plan-team-distribution.js +0 -71
- package/dist/tools/reconcile-status-json.d.ts +0 -4
- package/dist/tools/reconcile-status-json.js +0 -209
- package/dist/tools/register-all-tools.d.ts +0 -8
- package/dist/tools/register-all-tools.js +0 -239
- package/dist/tools/tool-registry/group-analysis-monitoring.d.ts +0 -3
- package/dist/tools/tool-registry/group-analysis-monitoring.js +0 -942
- package/dist/tools/tool-registry/group-integrations.d.ts +0 -3
- package/dist/tools/tool-registry/group-integrations.js +0 -1046
- package/dist/tools/tool-registry/group-misc.d.ts +0 -3
- package/dist/tools/tool-registry/group-misc.js +0 -1367
- package/dist/tools/tool-registry/group-platform.d.ts +0 -3
- package/dist/tools/tool-registry/group-platform.js +0 -1681
- package/dist/tools/tool-registry/group-session-knowledge.d.ts +0 -3
- package/dist/tools/tool-registry/group-session-knowledge.js +0 -1416
- package/dist/tools/tool-registry/group-spec-ops.d.ts +0 -3
- package/dist/tools/tool-registry/group-spec-ops.js +0 -917
- package/dist/tools/workspace-overview.d.ts +0 -4
- package/dist/tools/workspace-overview.js +0 -316
- package/dist/transports/middleware/index.d.ts +0 -9
- package/dist/transports/middleware/index.js +0 -7
- package/dist/transports/middleware/with-sandbox.d.ts +0 -21
- package/dist/transports/middleware/with-sandbox.js +0 -68
- package/dist/types/heal.d.ts +0 -18
- 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,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,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
|