@planu/cli 4.1.1 → 4.1.2
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 +11 -0
- package/dist/config/license-plans.json +65 -361
- 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,26 +0,0 @@
|
|
|
1
|
-
// testing/cassette/index.ts — Public API for LLM cassette/mock pattern (SPEC-743)
|
|
2
|
-
export { computeRequestSha } from './signature.js';
|
|
3
|
-
export { loadCassetteFromDisk as loadCassette, saveCassetteToDisk, upsertCassetteEntry, getCassettePath, } from './store.js';
|
|
4
|
-
export { createInterceptor, isRecordingMode, CassetteMissError } from './intercept.js';
|
|
5
|
-
import { upsertCassetteEntry } from './store.js';
|
|
6
|
-
/**
|
|
7
|
-
* withCassette — Higher-order helper for test cases.
|
|
8
|
-
* Runs a test function within a cassette scope.
|
|
9
|
-
* Actual interception must be installed by the caller via createInterceptor().
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* it('generates intake JSON', () => withCassette('handoff-artifacts/intake', async () => {
|
|
13
|
-
* const result = await generateIntakeJson(spec);
|
|
14
|
-
* expect(result).toMatchSnapshot();
|
|
15
|
-
* }));
|
|
16
|
-
*/
|
|
17
|
-
export async function withCassette(_cassetteName, fn) {
|
|
18
|
-
return fn();
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* recordCassette — Write a new entry to a cassette file (record mode only).
|
|
22
|
-
*/
|
|
23
|
-
export function recordCassette(name, entry) {
|
|
24
|
-
upsertCassetteEntry(name, entry);
|
|
25
|
-
}
|
|
26
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
export declare const ApplyDomainBundleInputSchema: {
|
|
3
|
-
bundle: z.ZodEnum<{
|
|
4
|
-
"react-native": "react-native";
|
|
5
|
-
"rest-api": "rest-api";
|
|
6
|
-
"stripe-payments": "stripe-payments";
|
|
7
|
-
"auth-supabase": "auth-supabase";
|
|
8
|
-
"nextjs-fullstack": "nextjs-fullstack";
|
|
9
|
-
}>;
|
|
10
|
-
projectPath: z.ZodString;
|
|
11
|
-
overwrite: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
12
|
-
};
|
|
13
|
-
export declare const ListDomainBundlesInputSchema: {
|
|
14
|
-
projectPath: z.ZodOptional<z.ZodString>;
|
|
15
|
-
};
|
|
16
|
-
interface ApplyDomainBundleArgs {
|
|
17
|
-
bundle: string;
|
|
18
|
-
projectPath: string;
|
|
19
|
-
overwrite?: boolean;
|
|
20
|
-
}
|
|
21
|
-
interface ListDomainBundlesArgs {
|
|
22
|
-
projectPath?: string;
|
|
23
|
-
}
|
|
24
|
-
export declare function handleApplyDomainBundle(args: ApplyDomainBundleArgs): Promise<{
|
|
25
|
-
content: {
|
|
26
|
-
type: 'text';
|
|
27
|
-
text: string;
|
|
28
|
-
}[];
|
|
29
|
-
}>;
|
|
30
|
-
export declare function handleListDomainBundles(_args: ListDomainBundlesArgs): Promise<{
|
|
31
|
-
content: {
|
|
32
|
-
type: 'text';
|
|
33
|
-
text: string;
|
|
34
|
-
}[];
|
|
35
|
-
}>;
|
|
36
|
-
export {};
|
|
37
|
-
//# sourceMappingURL=domain-bundle-handler.d.ts.map
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// Planu — domain-bundle-handler.ts
|
|
2
|
-
// Handlers for apply_domain_bundle and list_domain_bundles tools.
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { installBundle, listBundlesAsync } from '../engine/bundle-installer.js';
|
|
5
|
-
import { compactJson, compactResult } from './output-formatter.js';
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Zod schemas
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
export const ApplyDomainBundleInputSchema = {
|
|
10
|
-
bundle: z
|
|
11
|
-
.enum(['stripe-payments', 'auth-supabase', 'rest-api', 'nextjs-fullstack', 'react-native'])
|
|
12
|
-
.describe('Bundle ID to install. Valid values: stripe-payments, auth-supabase, rest-api, nextjs-fullstack, react-native'),
|
|
13
|
-
projectPath: z
|
|
14
|
-
.string()
|
|
15
|
-
.describe('Absolute path to the project where the bundle will be installed'),
|
|
16
|
-
overwrite: z
|
|
17
|
-
.boolean()
|
|
18
|
-
.optional()
|
|
19
|
-
.default(false)
|
|
20
|
-
.describe('If true, overwrite existing files. Default: false (skip existing files)'),
|
|
21
|
-
};
|
|
22
|
-
export const ListDomainBundlesInputSchema = {
|
|
23
|
-
projectPath: z
|
|
24
|
-
.string()
|
|
25
|
-
.optional()
|
|
26
|
-
.describe('Optional project path (unused, included for consistency)'),
|
|
27
|
-
};
|
|
28
|
-
function formatInstallResult(result) {
|
|
29
|
-
const lines = [`Bundle "${result.bundleId}" installation complete.`];
|
|
30
|
-
if (result.installed.length > 0) {
|
|
31
|
-
lines.push(`\nInstalled (${result.installed.length}):`);
|
|
32
|
-
for (const path of result.installed) {
|
|
33
|
-
lines.push(` + ${path}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (result.skipped.length > 0) {
|
|
37
|
-
lines.push(`\nSkipped — already exist (${result.skipped.length}):`);
|
|
38
|
-
for (const path of result.skipped) {
|
|
39
|
-
lines.push(` ~ ${path}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (result.errors.length > 0) {
|
|
43
|
-
lines.push(`\nErrors (${result.errors.length}):`);
|
|
44
|
-
for (const err of result.errors) {
|
|
45
|
-
lines.push(` ! ${err}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return lines.join('\n');
|
|
49
|
-
}
|
|
50
|
-
export async function handleApplyDomainBundle(args) {
|
|
51
|
-
const result = await installBundle(args.bundle, args.projectPath, {
|
|
52
|
-
overwrite: args.overwrite ?? false,
|
|
53
|
-
});
|
|
54
|
-
const summary = formatInstallResult(result);
|
|
55
|
-
const humanSummary = result.errors.length > 0
|
|
56
|
-
? `Bundle "${result.bundleId}" installed with ${result.errors.length} error(s). Check errors for details.`
|
|
57
|
-
: `Bundle "${result.bundleId}" installed successfully: ${result.installed.length} file(s) added, ${result.skipped.length} skipped.`;
|
|
58
|
-
return compactResult(compactJson({ ...result, summary, humanSummary }));
|
|
59
|
-
}
|
|
60
|
-
export async function handleListDomainBundles(_args) {
|
|
61
|
-
const bundles = await listBundlesAsync();
|
|
62
|
-
const items = bundles.map((b) => ({
|
|
63
|
-
id: b.id,
|
|
64
|
-
name: b.name,
|
|
65
|
-
description: b.description,
|
|
66
|
-
tags: b.tags,
|
|
67
|
-
}));
|
|
68
|
-
const humanSummary = `${items.length} domain bundles available: ${items.map((b) => b.name).join(', ')}.`;
|
|
69
|
-
return compactResult(compactJson({ bundles: items, humanSummary }));
|
|
70
|
-
}
|
|
71
|
-
//# sourceMappingURL=domain-bundle-handler.js.map
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// tools/figma/rules-file.ts — SPEC-488: generate_figma_rules_file handler
|
|
2
|
-
import { writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { getFigmaConfig, getFigmaCodeConnect, getFigmaTokens } from '../../storage/figma-store.js';
|
|
5
|
-
import { knowledgeStore } from '../../storage/index.js';
|
|
6
|
-
import { hashProjectPath } from '../../storage/base-store.js';
|
|
7
|
-
import { buildFigmaRulesFileContent } from '../../engine/figma/implementation-rules.js';
|
|
8
|
-
import { FIGMA_NOT_CONNECTED } from './shared.js';
|
|
9
|
-
export async function handleGenerateFigmaRulesFile(input) {
|
|
10
|
-
const config = await getFigmaConfig(input.projectPath);
|
|
11
|
-
if (config === null) {
|
|
12
|
-
return FIGMA_NOT_CONNECTED;
|
|
13
|
-
}
|
|
14
|
-
const projectId = hashProjectPath(input.projectPath);
|
|
15
|
-
const knowledge = await knowledgeStore.getKnowledge(projectId);
|
|
16
|
-
const [codeConnectEntries, tokens] = await Promise.all([
|
|
17
|
-
getFigmaCodeConnect(input.projectPath),
|
|
18
|
-
getFigmaTokens(input.projectPath),
|
|
19
|
-
]);
|
|
20
|
-
const content = buildFigmaRulesFileContent({
|
|
21
|
-
codeConnectEntries,
|
|
22
|
-
tokens,
|
|
23
|
-
stack: knowledge?.stack ?? [],
|
|
24
|
-
framework: knowledge?.framework ?? null,
|
|
25
|
-
});
|
|
26
|
-
const rulesDir = join(input.projectPath, '.claude', 'rules');
|
|
27
|
-
const rulesPath = join(rulesDir, 'figma-design-system.md');
|
|
28
|
-
await mkdir(rulesDir, { recursive: true });
|
|
29
|
-
await writeFile(rulesPath, content, 'utf-8');
|
|
30
|
-
const summary = [
|
|
31
|
-
`✓ Created .claude/rules/figma-design-system.md`,
|
|
32
|
-
` ${String(codeConnectEntries.length)} Code Connect components`,
|
|
33
|
-
` ${String(tokens.length)} design tokens`,
|
|
34
|
-
` Stack: ${knowledge?.framework ?? knowledge?.stack.slice(0, 3).join(', ') ?? 'unknown'}`,
|
|
35
|
-
].join('\n');
|
|
36
|
-
return {
|
|
37
|
-
content: [{ type: 'text', text: summary }],
|
|
38
|
-
structuredContent: {
|
|
39
|
-
rulesPath,
|
|
40
|
-
codeConnectCount: codeConnectEntries.length,
|
|
41
|
-
tokenCount: tokens.length,
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
//# sourceMappingURL=rules-file.js.map
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import type { HealRunResult } from '../types/heal.js';
|
|
3
|
-
export declare function healPlanuRoot(opts: {
|
|
4
|
-
projectRoot: string;
|
|
5
|
-
dryRun?: boolean;
|
|
6
|
-
}): Promise<HealRunResult>;
|
|
7
|
-
export declare function registerHealPlanuRootTool(server: McpServer): void;
|
|
8
|
-
//# sourceMappingURL=heal-planu-root.d.ts.map
|
|
@@ -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
|