claude-flow 3.49.0 → 3.51.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.
- package/.claude/helpers/hook-handler.cjs +14 -1
- package/.claude/proven-config.json +1 -1
- package/.claude/settings.json +11 -2
- package/.claude-plugin/marketplace.json +15 -0
- package/package.json +1 -1
- package/v3/@claude-flow/cli/README.md +0 -2
- package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
- package/v3/@claude-flow/cli/dist/src/commands/catalog.d.ts +18 -0
- package/v3/@claude-flow/cli/dist/src/commands/catalog.js +89 -0
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +24 -1
- package/v3/@claude-flow/cli/dist/src/commands/index.js +5 -0
- package/v3/@claude-flow/cli/dist/src/commands/init.js +60 -1
- package/v3/@claude-flow/cli/dist/src/commands/mission.d.ts +24 -0
- package/v3/@claude-flow/cli/dist/src/commands/mission.js +116 -0
- package/v3/@claude-flow/cli/dist/src/commands/mods.d.ts +13 -0
- package/v3/@claude-flow/cli/dist/src/commands/mods.js +170 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +12 -0
- package/v3/@claude-flow/cli/dist/src/mcp-client.js +3 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/capability-brain.js +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/mission-tools.d.ts +22 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/mission-tools.js +75 -0
- package/v3/@claude-flow/cli/dist/src/missions/fold.d.ts +19 -0
- package/v3/@claude-flow/cli/dist/src/missions/fold.js +238 -0
- package/v3/@claude-flow/cli/dist/src/missions/index.d.ts +10 -0
- package/v3/@claude-flow/cli/dist/src/missions/index.js +10 -0
- package/v3/@claude-flow/cli/dist/src/missions/observation.d.ts +70 -0
- package/v3/@claude-flow/cli/dist/src/missions/observation.js +77 -0
- package/v3/@claude-flow/cli/dist/src/missions/schemas.d.ts +735 -0
- package/v3/@claude-flow/cli/dist/src/missions/schemas.js +150 -0
- package/v3/@claude-flow/cli/dist/src/missions/service.d.ts +88 -0
- package/v3/@claude-flow/cli/dist/src/missions/service.js +278 -0
- package/v3/@claude-flow/cli/dist/src/missions/store.d.ts +68 -0
- package/v3/@claude-flow/cli/dist/src/missions/store.js +209 -0
- package/v3/@claude-flow/cli/dist/src/missions/transitions.d.ts +81 -0
- package/v3/@claude-flow/cli/dist/src/missions/transitions.js +178 -0
- package/v3/@claude-flow/cli/dist/src/mods/apply.d.ts +50 -0
- package/v3/@claude-flow/cli/dist/src/mods/apply.js +186 -0
- package/v3/@claude-flow/cli/dist/src/mods/claude-installs.d.ts +29 -0
- package/v3/@claude-flow/cli/dist/src/mods/claude-installs.js +83 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/catalog.d.ts +45 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/catalog.js +234 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/engine-report.d.ts +66 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/engine-report.js +119 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/generate.d.ts +51 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/generate.js +166 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/index.d.ts +9 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/index.js +9 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/inventory.d.ts +63 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/inventory.js +230 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +836 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.js +137 -0
- package/v3/@claude-flow/cli/dist/src/mods/install.d.ts +102 -0
- package/v3/@claude-flow/cli/dist/src/mods/install.js +185 -0
- package/v3/@claude-flow/cli/dist/src/mods/plugin-repair.d.ts +81 -0
- package/v3/@claude-flow/cli/dist/src/mods/plugin-repair.js +120 -0
- package/v3/@claude-flow/cli/dist/src/mods/plugin-resolve.d.ts +63 -0
- package/v3/@claude-flow/cli/dist/src/mods/plugin-resolve.js +182 -0
- package/v3/@claude-flow/cli/dist/src/mods/policy-projection.d.ts +39 -0
- package/v3/@claude-flow/cli/dist/src/mods/policy-projection.js +65 -0
- package/v3/@claude-flow/cli/dist/src/mods/probe.d.ts +33 -0
- package/v3/@claude-flow/cli/dist/src/mods/probe.js +143 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +5 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +20 -1
- package/v3/@claude-flow/cli/package.json +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo mods` — opt into running ruflo as a Claude Code mod (ADR-404,
|
|
3
|
+
* Claude Code function hooks, early access).
|
|
4
|
+
*
|
|
5
|
+
* install/uninstall edit settings files and record what they added;
|
|
6
|
+
* status/doctor report what can be known from outside a session; sync-policy
|
|
7
|
+
* rewrites the policy projection the mod's tool check reads. Classic hooks
|
|
8
|
+
* are never removed: they stay the default and the fallback.
|
|
9
|
+
*/
|
|
10
|
+
import { output } from '../output.js';
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { join, resolve } from 'node:path';
|
|
13
|
+
import { directoryMarketplace, MARKETPLACE_NAME } from '../mods/install.js';
|
|
14
|
+
import { probeMods } from '../mods/probe.js';
|
|
15
|
+
import { applyMods, removeMods } from '../mods/apply.js';
|
|
16
|
+
/**
|
|
17
|
+
* `--source local` / `--marketplace-path <dir>`: a ruflo checkout as a
|
|
18
|
+
* directory marketplace (dogfooding). It must be a ruflo marketplace.
|
|
19
|
+
*/
|
|
20
|
+
function localMarketplace(dir) {
|
|
21
|
+
const catalog = join(resolve(dir), '.claude-plugin', 'marketplace.json');
|
|
22
|
+
if (!existsSync(catalog))
|
|
23
|
+
return `${dir} has no .claude-plugin/marketplace.json`;
|
|
24
|
+
try {
|
|
25
|
+
if (JSON.parse(readFileSync(catalog, 'utf8')).name !== MARKETPLACE_NAME)
|
|
26
|
+
return `${catalog} is not the ${MARKETPLACE_NAME} marketplace`;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return `${catalog} is not JSON`;
|
|
30
|
+
}
|
|
31
|
+
return directoryMarketplace(dir);
|
|
32
|
+
}
|
|
33
|
+
function projectRoot(ctx) {
|
|
34
|
+
return ctx.flags.projectRoot ?? ctx.flags['project-root'] ?? ctx.cwd ?? process.cwd();
|
|
35
|
+
}
|
|
36
|
+
function printFindings(findings) {
|
|
37
|
+
for (const f of findings) {
|
|
38
|
+
const mark = f.status === 'pass' ? output.success('✓') : f.status === 'warn' ? output.warning('!') : output.error('✗');
|
|
39
|
+
output.writeln(`${mark} ${f.name}: ${f.message}`);
|
|
40
|
+
if (f.fix && f.status !== 'pass')
|
|
41
|
+
output.writeln(output.dim(` fix: ${f.fix}`));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const rootOption = { name: 'project-root', description: 'Project root (default: current directory)', type: 'string' };
|
|
45
|
+
const installSub = {
|
|
46
|
+
name: 'install',
|
|
47
|
+
description: 'Enable the ruflo mod plugins (ruflo-mods, ruflo-swarm, ruflo-console) for this project; classic hooks stay as fallback',
|
|
48
|
+
options: [
|
|
49
|
+
rootOption,
|
|
50
|
+
{ name: 'scope', description: 'local (.claude/settings.local.json, default) | project (.claude/settings.json)', type: 'string', default: 'local' },
|
|
51
|
+
{ name: 'dry-run', description: 'Show the settings that would be written and the claude commands that would run', type: 'boolean', default: false },
|
|
52
|
+
{ name: 'plugin-install', description: 'Refresh the ruflo marketplace and run `claude plugin install` (--no-plugin-install to only write settings)', type: 'boolean', default: true },
|
|
53
|
+
{ name: 'strict', description: 'Exit 1 when a required plugin could not be made loadable', type: 'boolean', default: false },
|
|
54
|
+
{ name: 'source', description: 'github (ruvnet/ruflo, default) | local (a ruflo checkout as a directory marketplace: plugins load live from it)', type: 'string', default: 'github' },
|
|
55
|
+
{ name: 'marketplace-path', description: 'With --source local: the ruflo checkout (default: the project root)', type: 'string' },
|
|
56
|
+
],
|
|
57
|
+
action: async (ctx) => {
|
|
58
|
+
const scope = ctx.flags.scope ?? 'local';
|
|
59
|
+
if (scope !== 'local' && scope !== 'project') {
|
|
60
|
+
output.printError(`--scope must be local or project, got ${scope}`);
|
|
61
|
+
return { success: false, exitCode: 1 };
|
|
62
|
+
}
|
|
63
|
+
const source = ctx.flags.source ?? 'github';
|
|
64
|
+
const path = (ctx.flags.marketplacePath ?? ctx.flags['marketplace-path']);
|
|
65
|
+
if (source !== 'github' && source !== 'local') {
|
|
66
|
+
output.printError(`--source must be github or local, got ${source}`);
|
|
67
|
+
return { success: false, exitCode: 1 };
|
|
68
|
+
}
|
|
69
|
+
let marketplace;
|
|
70
|
+
if (source === 'local' || path) {
|
|
71
|
+
const local = localMarketplace(path ?? projectRoot(ctx));
|
|
72
|
+
if (typeof local === 'string') {
|
|
73
|
+
output.printError(`--source local: ${local}`);
|
|
74
|
+
return { success: false, exitCode: 1 };
|
|
75
|
+
}
|
|
76
|
+
marketplace = local;
|
|
77
|
+
}
|
|
78
|
+
const result = await applyMods(projectRoot(ctx), {
|
|
79
|
+
scope: scope,
|
|
80
|
+
marketplace,
|
|
81
|
+
dryRun: ctx.flags.dryRun === true || ctx.flags['dry-run'] === true,
|
|
82
|
+
pluginInstall: ctx.flags.pluginInstall !== false && ctx.flags['plugin-install'] !== false,
|
|
83
|
+
});
|
|
84
|
+
if (result.install.dryRun)
|
|
85
|
+
return { success: true, data: result };
|
|
86
|
+
if (result.resolvable)
|
|
87
|
+
output.writeln('Restart Claude Code, then run /ruflo in a session to open the console (/ruflo mods shows what the mod owns). Check with: ruflo mods doctor');
|
|
88
|
+
const data = { ...result.install, resolvable: result.resolvable };
|
|
89
|
+
return result.resolvable || ctx.flags.strict !== true ? { success: true, data } : { success: false, exitCode: 1, data };
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const uninstallSub = {
|
|
93
|
+
name: 'uninstall',
|
|
94
|
+
description: 'Remove what `ruflo mods install` added, and claude plugin uninstall what it installed (classic hooks take every event back)',
|
|
95
|
+
options: [rootOption, { name: 'dry-run', description: 'Show what would be removed', type: 'boolean', default: false }],
|
|
96
|
+
action: async (ctx) => {
|
|
97
|
+
const result = await removeMods(projectRoot(ctx), { dryRun: ctx.flags.dryRun === true || ctx.flags['dry-run'] === true });
|
|
98
|
+
if (!result.removed) {
|
|
99
|
+
output.printWarning('No install record (.claude-flow/mods/install.json): nothing ruflo added to remove.');
|
|
100
|
+
return { success: true, data: result };
|
|
101
|
+
}
|
|
102
|
+
output.printSuccess(`${result.dryRun ? 'Would remove' : 'Removed'} what ruflo mods added from ${result.settingsFiles.join(', ')}`);
|
|
103
|
+
if (result.uninstalled.length)
|
|
104
|
+
output.writeln(`claude plugin uninstall: ${result.uninstalled.join(', ')}`);
|
|
105
|
+
return { success: result.failed.length === 0, exitCode: result.failed.length ? 1 : 0, data: result };
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
function findingsCommand(name, description) {
|
|
109
|
+
return {
|
|
110
|
+
name,
|
|
111
|
+
description,
|
|
112
|
+
options: [rootOption, { name: 'json', description: 'Output as JSON', type: 'boolean', default: false }],
|
|
113
|
+
action: async (ctx) => {
|
|
114
|
+
const findings = probeMods({ projectRoot: projectRoot(ctx) });
|
|
115
|
+
if (ctx.flags.json)
|
|
116
|
+
output.printJson(findings);
|
|
117
|
+
else
|
|
118
|
+
printFindings(findings);
|
|
119
|
+
const failed = findings.some((f) => f.status === 'fail');
|
|
120
|
+
// status reports (exit 0); doctor gates on a failure (warnings are expected while early access is off).
|
|
121
|
+
return name === 'doctor' && failed ? { success: false, exitCode: 1, data: findings } : { success: true, data: findings };
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function syncPolicy(root, quiet) {
|
|
126
|
+
try {
|
|
127
|
+
const { loadPolicyState } = await import('../services/policy-runtime.js');
|
|
128
|
+
const { syncPolicyProjection } = await import('../mods/policy-projection.js');
|
|
129
|
+
const result = syncPolicyProjection(root, loadPolicyState(root));
|
|
130
|
+
if (!quiet || result.action !== 'unchanged')
|
|
131
|
+
output.writeln(`policy projection: ${result.action} (${result.path})`);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
output.printWarning(`policy projection not synced: ${error.message}`);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const syncPolicySub = {
|
|
140
|
+
name: 'sync-policy',
|
|
141
|
+
description: 'Rewrite the Claude Code policy projection from .claude-flow/policy/state.json',
|
|
142
|
+
options: [rootOption],
|
|
143
|
+
action: async (ctx) => {
|
|
144
|
+
const ok = await syncPolicy(projectRoot(ctx), false);
|
|
145
|
+
return { success: ok, exitCode: ok ? 0 : 1 };
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
const statusSub = findingsCommand('status', 'Show whether the mod is enabled, can load, and what it owns');
|
|
149
|
+
export const modsCommand = {
|
|
150
|
+
name: 'mods',
|
|
151
|
+
description: 'Run ruflo as a Claude Code mod (function hooks, early access, ADR-404)',
|
|
152
|
+
subcommands: [
|
|
153
|
+
installSub,
|
|
154
|
+
uninstallSub,
|
|
155
|
+
statusSub,
|
|
156
|
+
findingsCommand('doctor', 'Check the mod path; exits 1 only on a failure'),
|
|
157
|
+
syncPolicySub,
|
|
158
|
+
],
|
|
159
|
+
examples: [
|
|
160
|
+
{ command: 'ruflo mods install', description: 'Enable the mod plugins for this checkout (settings.local.json) and install them with claude' },
|
|
161
|
+
{ command: 'ruflo mods install --scope project', description: 'Same, in the committed settings.json (what ruflo init does)' },
|
|
162
|
+
{ command: 'ruflo mods install --no-plugin-install', description: 'Only write settings; run no claude command' },
|
|
163
|
+
{ command: 'ruflo mods install --source local', description: 'Dogfood: load the mods live from this ruflo checkout (directory marketplace)' },
|
|
164
|
+
{ command: 'ruflo mods doctor', description: 'Marketplace fresh? Plugins loadable? Function hooks on? Refused by policy?' },
|
|
165
|
+
{ command: 'ruflo mods uninstall', description: 'Remove only what install added' },
|
|
166
|
+
],
|
|
167
|
+
action: statusSub.action,
|
|
168
|
+
};
|
|
169
|
+
export default modsCommand;
|
|
170
|
+
//# sourceMappingURL=mods.js.map
|
|
@@ -530,7 +530,19 @@ export function generateHookHandler() {
|
|
|
530
530
|
' });',
|
|
531
531
|
'}',
|
|
532
532
|
'',
|
|
533
|
+
// ADR-404: same handshake as the shipped hook-handler.cjs, so a helper
|
|
534
|
+
// regenerated by the refresh fallback still hands route/post-edit to the mod.
|
|
535
|
+
'// ADR-404: the ruflo mod names the events it runs in-process in',
|
|
536
|
+
'// RUFLO_MODS_OWNS; only side-effect events hand over, guards always run.',
|
|
537
|
+
"const MOD_OWNABLE_EVENTS = new Set(['route', 'post-edit']);",
|
|
538
|
+
'function ownedByMod(cmd, env) {',
|
|
539
|
+
' env = env || process.env;',
|
|
540
|
+
' if (!MOD_OWNABLE_EVENTS.has(cmd)) return false;',
|
|
541
|
+
" return String(env.RUFLO_MODS_OWNS || '').split(',').some(function (owned) { return owned.trim() === cmd; });",
|
|
542
|
+
'}',
|
|
543
|
+
'',
|
|
533
544
|
'async function main() {',
|
|
545
|
+
' if (ownedByMod(command)) return;',
|
|
534
546
|
' let stdinData = "";',
|
|
535
547
|
' try { stdinData = await readStdin(); } catch (e) { /* ignore */ }',
|
|
536
548
|
' let hookInput = {};',
|
|
@@ -72,6 +72,7 @@ import { businessPodTools } from './mcp-tools/business-pod-tools.js';
|
|
|
72
72
|
// for ops-pod synthetic-endpoint benches). Default-rejects private addresses
|
|
73
73
|
// + auth headers; opt-in via CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE / _AUTH=1.
|
|
74
74
|
import { httpFetchTools } from './mcp-tools/http-fetch-tools.js';
|
|
75
|
+
import { missionTools } from './mcp-tools/mission-tools.js';
|
|
75
76
|
// #1916: coverage-aware routing tools — defined in ruvector/coverage-tools.ts
|
|
76
77
|
// but were never registered, so the `ruflo hooks coverage-*` CLI subcommands
|
|
77
78
|
// failed with `Tool not found: hooks_coverage-route`.
|
|
@@ -180,6 +181,8 @@ registerTools([
|
|
|
180
181
|
...businessPodTools,
|
|
181
182
|
// ADR-164 Phase 4 §5.1.8 — http_fetch (1 tool, secure-by-default HTTP probe)
|
|
182
183
|
...httpFetchTools,
|
|
184
|
+
// ADR-406 — mission semantic operations (create/plan/get/events/request_action)
|
|
185
|
+
...missionTools,
|
|
183
186
|
]);
|
|
184
187
|
// The capability brain consumes the completed live registry. This is injected
|
|
185
188
|
// after registration to keep guidance honest and avoid a registry import cycle.
|
|
@@ -314,7 +314,7 @@ export const CAPABILITY_DOMAINS = [
|
|
|
314
314
|
{
|
|
315
315
|
id: 'tasks-workflows-sessions',
|
|
316
316
|
name: 'Tasks, Workflows, Sessions & Progress',
|
|
317
|
-
prefixes: ['task_', 'workflow_', 'session_', 'progress_'],
|
|
317
|
+
prefixes: ['task_', 'workflow_', 'session_', 'progress_', 'mission_'],
|
|
318
318
|
description: 'Task orchestration, reusable workflows, session state, progress, and handoff.',
|
|
319
319
|
taskSignals: ['task', 'workflow', 'session', 'handoff', 'progress'],
|
|
320
320
|
commands: ['task create', 'task status', 'workflow run'],
|
|
@@ -38,4 +38,5 @@ export { xFederationJoinTools } from './x-federation-join.js';
|
|
|
38
38
|
export { xFederationChannelTools } from './x-federation-channels.js';
|
|
39
39
|
export { businessPodTools } from './business-pod-tools.js';
|
|
40
40
|
export { httpFetchTools } from './http-fetch-tools.js';
|
|
41
|
+
export { missionTools, createMissionTools } from './mission-tools.js';
|
|
41
42
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -43,4 +43,6 @@ export { xFederationChannelTools } from './x-federation-channels.js';
|
|
|
43
43
|
export { businessPodTools } from './business-pod-tools.js';
|
|
44
44
|
// ADR-164 Phase 4 §5.1.8 — http_fetch (secure-by-default HTTP probe)
|
|
45
45
|
export { httpFetchTools } from './http-fetch-tools.js';
|
|
46
|
+
// ADR-406 — mission semantic operations
|
|
47
|
+
export { missionTools, createMissionTools } from './mission-tools.js';
|
|
46
48
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-406 §19.7 — MCP adapter for the mission semantic operations.
|
|
3
|
+
*
|
|
4
|
+
* A thin transport: input goes to the same `MissionService` (and the same zod
|
|
5
|
+
* schemas) the CLI uses, unchanged. Policy authorization of the tool call
|
|
6
|
+
* itself happens in `mcp-client.ts` before any handler runs; nothing here
|
|
7
|
+
* accepts an approval, principal or permission flag from input.
|
|
8
|
+
*/
|
|
9
|
+
import type { MCPTool } from './types.js';
|
|
10
|
+
import { type MissionResult } from '../missions/service.js';
|
|
11
|
+
/** The operations an adapter needs; London-school tests substitute it. */
|
|
12
|
+
export interface MissionPort {
|
|
13
|
+
create(raw: unknown): Promise<MissionResult<unknown>>;
|
|
14
|
+
plan(raw: unknown): Promise<MissionResult<unknown>>;
|
|
15
|
+
get(raw: unknown): Promise<MissionResult<unknown>>;
|
|
16
|
+
events(raw: unknown): Promise<MissionResult<unknown>>;
|
|
17
|
+
requestAction(raw: unknown): Promise<MissionResult<unknown>>;
|
|
18
|
+
}
|
|
19
|
+
export type MissionPortFactory = (projectRoot: string) => MissionPort;
|
|
20
|
+
export declare function createMissionTools(factory?: MissionPortFactory): MCPTool[];
|
|
21
|
+
export declare const missionTools: MCPTool[];
|
|
22
|
+
//# sourceMappingURL=mission-tools.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-406 §19.7 — MCP adapter for the mission semantic operations.
|
|
3
|
+
*
|
|
4
|
+
* A thin transport: input goes to the same `MissionService` (and the same zod
|
|
5
|
+
* schemas) the CLI uses, unchanged. Policy authorization of the tool call
|
|
6
|
+
* itself happens in `mcp-client.ts` before any handler runs; nothing here
|
|
7
|
+
* accepts an approval, principal or permission flag from input.
|
|
8
|
+
*/
|
|
9
|
+
import { getProjectCwd } from './types.js';
|
|
10
|
+
import { MissionService } from '../missions/service.js';
|
|
11
|
+
const defaultFactory = (projectRoot) => new MissionService({ projectRoot, channel: 'mcp' });
|
|
12
|
+
function rootOf(context) {
|
|
13
|
+
return typeof context?.projectRoot === 'string' ? context.projectRoot : getProjectCwd();
|
|
14
|
+
}
|
|
15
|
+
const MISSION_ID = { type: 'string', pattern: '^msn_[a-f0-9]{24}$', description: 'Mission id' };
|
|
16
|
+
const REQUEST_ID = { type: 'string', maxLength: 128, description: 'Caller-chosen idempotency key; reuse only to retry the same request' };
|
|
17
|
+
const EXPECTED_REVISION = { type: 'integer', minimum: 1, description: 'Mission revision the request was prepared against' };
|
|
18
|
+
export function createMissionTools(factory = defaultFactory) {
|
|
19
|
+
const call = (op) => async (input, context) => factory(rootOf(context))[op](input);
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
name: 'mission_create',
|
|
23
|
+
description: 'Create a draft mission (objective only), idempotent per requestId. Use when starting governed multi-step work that several clients (CLI, MCP, the Claude Code workbench) must observe and control through one durable record. task_create is wrong because a task has no plan revision, budget ceiling or acceptance evidence. Recording a mission executes nothing.',
|
|
24
|
+
category: 'mission',
|
|
25
|
+
inputSchema: { type: 'object', properties: { requestId: REQUEST_ID, objective: { type: 'string', maxLength: 2000 } }, required: ['requestId', 'objective'] },
|
|
26
|
+
handler: call('create'),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'mission_plan',
|
|
30
|
+
description: 'Submit or revise a mission plan: acyclic task graph, acceptance criteria and budget ceiling in integer minor units. Use when a mission needs a reviewable plan before any authorization or admission. Editing plan state through memory_store is wrong because it bypasses revision checks: a stale expectedRevision here returns a conflict with the current revision, and a revision invalidates prior authorization.',
|
|
31
|
+
category: 'mission',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: { requestId: REQUEST_ID, missionId: MISSION_ID, expectedRevision: EXPECTED_REVISION, plan: { type: 'object', description: '{ tasks[], acceptance[], budget{currency, ceilingMinor}, scope }' } },
|
|
35
|
+
required: ['requestId', 'missionId', 'expectedRevision', 'plan'],
|
|
36
|
+
},
|
|
37
|
+
handler: call('plan'),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'mission_get',
|
|
41
|
+
description: 'Read one mission (record, plan, budget, tasks, evidence, executor observation) or list missions; read only. Use when you need the authoritative current state of a mission before acting on it. Inferring state from task_status or a UI badge is wrong because task status is recorded state; only evidence.verified counts as verified, and a disconnected executor is not a failure.',
|
|
42
|
+
category: 'mission',
|
|
43
|
+
inputSchema: { type: 'object', properties: { missionId: MISSION_ID } },
|
|
44
|
+
handler: call('get'),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'mission_events',
|
|
48
|
+
description: 'Read mission events after a durable cursor (afterSequence). Use when resuming or reconnecting a client and you need exactly what changed since your last sequence. Re-reading mission_get in a loop is wrong because it loses the transition history; delivery here may repeat, so deduplicate by (missionId, seq), and gap=true means reload with mission_get before replaying.',
|
|
49
|
+
category: 'mission',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: { missionId: MISSION_ID, afterSequence: { type: 'integer', minimum: 0 }, limit: { type: 'integer', minimum: 1, maximum: 500 } },
|
|
53
|
+
required: ['missionId'],
|
|
54
|
+
},
|
|
55
|
+
handler: call('events'),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'mission_request_action',
|
|
59
|
+
description: 'Request a scoped mission control action: requestAuthorization, pause or cancel (admit and resume report executor-unavailable until a durable executor is admitted). Use when a person or agent wants a running or planned mission to change course. Calling task_cancel or killing a process is wrong because it skips the revision check and executor acknowledgement; a request is not authorization, the runtime decides.',
|
|
60
|
+
category: 'mission',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
requestId: REQUEST_ID, missionId: MISSION_ID, expectedRevision: EXPECTED_REVISION,
|
|
65
|
+
action: { type: 'string', enum: ['requestAuthorization', 'pause', 'cancel', 'admit', 'resume'] },
|
|
66
|
+
reason: { type: 'string', maxLength: 500 },
|
|
67
|
+
},
|
|
68
|
+
required: ['requestId', 'missionId', 'expectedRevision', 'action'],
|
|
69
|
+
},
|
|
70
|
+
handler: call('requestAction'),
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
export const missionTools = createMissionTools();
|
|
75
|
+
//# sourceMappingURL=mission-tools.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-406 §19.3 — fold a mission's append-only event log into a `MissionView`.
|
|
3
|
+
*
|
|
4
|
+
* Replay re-checks every control event against the transition table and every
|
|
5
|
+
* observation against its own rules, and verifies the hash chain, so state is
|
|
6
|
+
* a pure function of the log: snapshot + tail and full replay must agree.
|
|
7
|
+
*/
|
|
8
|
+
import { type ControlEventType, type MissionEvent, type ObservationEventType } from './schemas.js';
|
|
9
|
+
import { type MissionView } from './transitions.js';
|
|
10
|
+
export declare const GENESIS_HASH: string;
|
|
11
|
+
export declare function isControlEvent(type: string): type is ControlEventType;
|
|
12
|
+
/** Hash over every field except `hash` itself, chained through `prevHash`. */
|
|
13
|
+
export declare function eventHash(event: Omit<MissionEvent, 'hash'>): string;
|
|
14
|
+
/** Observation rules; throws `TransitionError`. Observations never change state. */
|
|
15
|
+
export declare function checkObservation(view: MissionView, type: ObservationEventType, p: Record<string, unknown>): void;
|
|
16
|
+
/** Apply one event; throws `TransitionError` for anything the rules refuse. */
|
|
17
|
+
export declare function foldEvent(view: MissionView | null, event: MissionEvent): MissionView;
|
|
18
|
+
export declare function replay(events: readonly MissionEvent[], from?: MissionView | null): MissionView | null;
|
|
19
|
+
//# sourceMappingURL=fold.d.ts.map
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-406 §19.3 — fold a mission's append-only event log into a `MissionView`.
|
|
3
|
+
*
|
|
4
|
+
* Replay re-checks every control event against the transition table and every
|
|
5
|
+
* observation against its own rules, and verifies the hash chain, so state is
|
|
6
|
+
* a pure function of the log: snapshot + tail and full replay must agree.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { canonicalJson } from '../mods/command-registry/catalog.js';
|
|
10
|
+
import { CONTROL_EVENTS, MISSION_EVENT_CONTRACT, MISSION_SCHEMA_VERSION, executorConnectionSchema, missionRecordSchema, } from './schemas.js';
|
|
11
|
+
import { TransitionError, checkTransition, digestOf } from './transitions.js';
|
|
12
|
+
import { TERMINAL_STATES } from './schemas.js';
|
|
13
|
+
export const GENESIS_HASH = `sha256:${'0'.repeat(64)}`;
|
|
14
|
+
const TASK_STATUSES = new Set(['pending', 'running', 'recorded-done', 'failed', 'unknown']);
|
|
15
|
+
export function isControlEvent(type) {
|
|
16
|
+
return CONTROL_EVENTS.includes(type);
|
|
17
|
+
}
|
|
18
|
+
/** Hash over every field except `hash` itself, chained through `prevHash`. */
|
|
19
|
+
export function eventHash(event) {
|
|
20
|
+
return `sha256:${createHash('sha256').update(canonicalJson(event)).digest('hex')}`;
|
|
21
|
+
}
|
|
22
|
+
/** Observation rules; throws `TransitionError`. Observations never change state. */
|
|
23
|
+
export function checkObservation(view, type, p) {
|
|
24
|
+
if (TERMINAL_STATES.has(view.record.state))
|
|
25
|
+
throw new TransitionError('terminal', `mission is ${view.record.state}`);
|
|
26
|
+
if (type === 'executor.observed') {
|
|
27
|
+
if (typeof p.executorId !== 'string' || !p.executorId)
|
|
28
|
+
throw new TransitionError('invalid-observation', 'executorId required');
|
|
29
|
+
if (!executorConnectionSchema.safeParse(p.connection).success)
|
|
30
|
+
throw new TransitionError('invalid-observation', 'connection must be healthy|stale|disconnected|unknown');
|
|
31
|
+
if (typeof p.observedAt !== 'string')
|
|
32
|
+
throw new TransitionError('invalid-observation', 'observedAt (source time) required');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (type === 'task.observed') {
|
|
36
|
+
if (!view.planBody?.tasks.some((t) => t.id === p.taskId))
|
|
37
|
+
throw new TransitionError('invalid-observation', `unknown task ${String(p.taskId)}`);
|
|
38
|
+
if (!TASK_STATUSES.has(p.status))
|
|
39
|
+
throw new TransitionError('invalid-observation', 'invalid task status');
|
|
40
|
+
if (p.leaseEpoch !== view.lease.epoch)
|
|
41
|
+
throw new TransitionError('stale-epoch', `lease epoch ${String(p.leaseEpoch)} is not current (${view.lease.epoch})`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// evidence.recorded
|
|
45
|
+
if (!view.planBody?.acceptance.some((c) => c.id === p.criterionId))
|
|
46
|
+
throw new TransitionError('invalid-observation', `unknown criterion ${String(p.criterionId)}`);
|
|
47
|
+
if (typeof p.evidenceId !== 'string' || typeof p.producer !== 'string')
|
|
48
|
+
throw new TransitionError('invalid-observation', 'evidenceId and producer required');
|
|
49
|
+
if (typeof p.artifactDigest !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(p.artifactDigest))
|
|
50
|
+
throw new TransitionError('invalid-observation', 'artifactDigest required');
|
|
51
|
+
if (typeof p.verified !== 'boolean')
|
|
52
|
+
throw new TransitionError('invalid-observation', 'verified flag required');
|
|
53
|
+
if (view.evidence.some((e) => e.evidenceId === p.evidenceId))
|
|
54
|
+
throw new TransitionError('invalid-observation', `duplicate evidence ${String(p.evidenceId)}`);
|
|
55
|
+
}
|
|
56
|
+
function created(event) {
|
|
57
|
+
const p = event.payload;
|
|
58
|
+
const record = missionRecordSchema.parse({
|
|
59
|
+
schemaVersion: MISSION_SCHEMA_VERSION,
|
|
60
|
+
missionId: event.missionId,
|
|
61
|
+
tenantId: p.tenantId,
|
|
62
|
+
workspaceId: p.workspaceId,
|
|
63
|
+
ownerPrincipalId: p.ownerPrincipalId,
|
|
64
|
+
revision: 1,
|
|
65
|
+
objective: p.objective,
|
|
66
|
+
plan: { revision: 0, digest: 'none', taskGraphRef: 'none' },
|
|
67
|
+
policyRef: p.policyRef,
|
|
68
|
+
budgetRef: p.budgetRef,
|
|
69
|
+
executionMode: 'session-bound',
|
|
70
|
+
state: 'draft',
|
|
71
|
+
evidenceRefs: [],
|
|
72
|
+
acceptance: { revision: 0, criteriaDigest: 'none' },
|
|
73
|
+
lastEventSequence: event.seq,
|
|
74
|
+
createdAt: event.at,
|
|
75
|
+
updatedAt: event.at,
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
record, planBody: null, budget: null, tasks: {}, evidence: [], executor: null,
|
|
79
|
+
lease: { executorId: null, epoch: 0 }, unresolvedOperations: [], blockedReason: null, lastHash: event.hash,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function withPlan(view, plan, event) {
|
|
83
|
+
const criteriaDigest = digestOf(plan.acceptance);
|
|
84
|
+
const prior = view.record.acceptance;
|
|
85
|
+
const estimated = plan.tasks.reduce((s, t) => s + t.estimatedCostMinor, 0);
|
|
86
|
+
const tasks = {};
|
|
87
|
+
for (const t of plan.tasks)
|
|
88
|
+
tasks[t.id] = { status: view.tasks[t.id]?.status ?? 'pending' };
|
|
89
|
+
return {
|
|
90
|
+
...view,
|
|
91
|
+
planBody: plan,
|
|
92
|
+
tasks,
|
|
93
|
+
budget: {
|
|
94
|
+
currency: plan.budget.currency,
|
|
95
|
+
ceilingMinor: plan.budget.ceilingMinor,
|
|
96
|
+
estimatedMinor: estimated,
|
|
97
|
+
reservedMinor: view.budget?.reservedMinor ?? 0,
|
|
98
|
+
settledMinor: view.budget?.settledMinor ?? 0,
|
|
99
|
+
unresolvedMinor: view.budget?.unresolvedMinor ?? 0,
|
|
100
|
+
},
|
|
101
|
+
record: {
|
|
102
|
+
...view.record,
|
|
103
|
+
plan: { revision: view.record.plan.revision + 1, digest: String(event.payload.digest), taskGraphRef: `event:${event.seq}` },
|
|
104
|
+
acceptance: { revision: criteriaDigest === prior.criteriaDigest ? prior.revision : prior.revision + 1, criteriaDigest },
|
|
105
|
+
executionMode: plan.tasks.some((t) => t.executor.mode === 'session-bound') ? 'session-bound' : 'durable-executor',
|
|
106
|
+
// §19.4: a new plan revision invalidates authorization for the old one.
|
|
107
|
+
authorizationRef: undefined,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function applyControl(view, event) {
|
|
112
|
+
const rule = checkTransition(view, event.type, event.payload);
|
|
113
|
+
if (event.revision !== view.record.revision + 1) {
|
|
114
|
+
throw new TransitionError('revision-mismatch', `control event ${event.seq} must advance revision to ${view.record.revision + 1}`);
|
|
115
|
+
}
|
|
116
|
+
if (event.expectedRevision !== view.record.revision) {
|
|
117
|
+
throw new TransitionError('revision-conflict', `expected ${String(event.expectedRevision)}, current ${view.record.revision}`);
|
|
118
|
+
}
|
|
119
|
+
let next = view;
|
|
120
|
+
const p = event.payload;
|
|
121
|
+
switch (event.type) {
|
|
122
|
+
case 'plan.validated':
|
|
123
|
+
case 'plan.revised':
|
|
124
|
+
next = withPlan(view, p.plan, event);
|
|
125
|
+
break;
|
|
126
|
+
case 'admission.accepted':
|
|
127
|
+
case 'resume.admitted':
|
|
128
|
+
next = {
|
|
129
|
+
...view,
|
|
130
|
+
budget: view.budget && { ...view.budget, reservedMinor: view.budget.reservedMinor + Number(p.reservationMinor) },
|
|
131
|
+
// A new admission fences every executor holding an older epoch.
|
|
132
|
+
lease: { executorId: null, epoch: view.lease.epoch + 1 },
|
|
133
|
+
blockedReason: null,
|
|
134
|
+
record: { ...view.record, authorizationRef: String(p.authorizationRef) },
|
|
135
|
+
};
|
|
136
|
+
break;
|
|
137
|
+
case 'executor.acknowledged':
|
|
138
|
+
next = { ...view, lease: { executorId: String(p.executorId), epoch: view.lease.epoch } };
|
|
139
|
+
break;
|
|
140
|
+
case 'blocked':
|
|
141
|
+
next = { ...view, blockedReason: String(p.reason) };
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
const record = { ...next.record };
|
|
147
|
+
if (record.authorizationRef === undefined)
|
|
148
|
+
delete record.authorizationRef;
|
|
149
|
+
return {
|
|
150
|
+
...next,
|
|
151
|
+
record: { ...record, state: rule.to, revision: view.record.revision + 1, lastEventSequence: event.seq, updatedAt: event.at },
|
|
152
|
+
lastHash: event.hash,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function applyObservation(view, event) {
|
|
156
|
+
checkObservation(view, event.type, event.payload);
|
|
157
|
+
const p = event.payload;
|
|
158
|
+
let next = view;
|
|
159
|
+
if (event.type === 'executor.observed') {
|
|
160
|
+
next = {
|
|
161
|
+
...view,
|
|
162
|
+
executor: {
|
|
163
|
+
executorId: String(p.executorId),
|
|
164
|
+
connection: String(p.connection),
|
|
165
|
+
observedAt: String(p.observedAt),
|
|
166
|
+
lastAckExecutionId: typeof p.lastAckExecutionId === 'string' ? p.lastAckExecutionId : view.executor?.lastAckExecutionId ?? null,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
else if (event.type === 'task.observed') {
|
|
171
|
+
const id = String(p.taskId);
|
|
172
|
+
const status = p.status;
|
|
173
|
+
const op = `task:${id}`;
|
|
174
|
+
const unresolved = view.unresolvedOperations.filter((o) => o !== op);
|
|
175
|
+
next = {
|
|
176
|
+
...view,
|
|
177
|
+
tasks: { ...view.tasks, [id]: { status, ...(typeof p.invocationId === 'string' ? { invocationId: p.invocationId } : {}) } },
|
|
178
|
+
// An unknown outcome stays unresolved until a later observation settles it.
|
|
179
|
+
unresolvedOperations: status === 'unknown' ? [...unresolved, op] : unresolved,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
next = {
|
|
184
|
+
...view,
|
|
185
|
+
evidence: [...view.evidence, {
|
|
186
|
+
evidenceId: String(p.evidenceId),
|
|
187
|
+
criterionId: String(p.criterionId),
|
|
188
|
+
taskId: typeof p.taskId === 'string' ? p.taskId : null,
|
|
189
|
+
artifactDigest: String(p.artifactDigest),
|
|
190
|
+
producer: String(p.producer),
|
|
191
|
+
verified: p.verified === true,
|
|
192
|
+
planRevision: view.record.plan.revision,
|
|
193
|
+
criteriaDigest: view.record.acceptance.criteriaDigest,
|
|
194
|
+
at: event.at,
|
|
195
|
+
}],
|
|
196
|
+
record: { ...view.record, evidenceRefs: [...view.record.evidenceRefs, String(p.evidenceId)] },
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
...next,
|
|
201
|
+
record: { ...next.record, lastEventSequence: event.seq, updatedAt: event.at },
|
|
202
|
+
lastHash: event.hash,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/** Apply one event; throws `TransitionError` for anything the rules refuse. */
|
|
206
|
+
export function foldEvent(view, event) {
|
|
207
|
+
if (event.contract !== MISSION_EVENT_CONTRACT)
|
|
208
|
+
throw new TransitionError('bad-contract', `unknown event contract ${String(event.contract)}`);
|
|
209
|
+
const expectedPrev = view ? view.lastHash : GENESIS_HASH;
|
|
210
|
+
if (event.prevHash !== expectedPrev)
|
|
211
|
+
throw new TransitionError('chain-broken', `event ${event.seq} prevHash mismatch`);
|
|
212
|
+
const { hash, ...rest } = event;
|
|
213
|
+
if (eventHash(rest) !== hash)
|
|
214
|
+
throw new TransitionError('chain-broken', `event ${event.seq} hash mismatch`);
|
|
215
|
+
const expectedSeq = view ? view.record.lastEventSequence + 1 : 1;
|
|
216
|
+
if (event.seq !== expectedSeq)
|
|
217
|
+
throw new TransitionError('sequence-gap', `event ${event.seq}, expected ${expectedSeq}`);
|
|
218
|
+
if (!view) {
|
|
219
|
+
if (event.type !== 'mission.created')
|
|
220
|
+
throw new TransitionError('invalid-transition', 'first event must be mission.created');
|
|
221
|
+
return created(event);
|
|
222
|
+
}
|
|
223
|
+
if (event.type === 'mission.created')
|
|
224
|
+
throw new TransitionError('invalid-transition', 'mission already created');
|
|
225
|
+
if (isControlEvent(event.type))
|
|
226
|
+
return applyControl(view, event);
|
|
227
|
+
if (event.expectedRevision !== null || event.revision !== view.record.revision) {
|
|
228
|
+
throw new TransitionError('invalid-observation', 'observations carry no expected revision and do not change it');
|
|
229
|
+
}
|
|
230
|
+
return applyObservation(view, event);
|
|
231
|
+
}
|
|
232
|
+
export function replay(events, from = null) {
|
|
233
|
+
let view = from;
|
|
234
|
+
for (const e of events)
|
|
235
|
+
view = foldEvent(view, e);
|
|
236
|
+
return view;
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=fold.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-406 mission control runtime (M0 contract + storage, M1 observation).
|
|
3
|
+
*/
|
|
4
|
+
export * from './schemas.js';
|
|
5
|
+
export * from './transitions.js';
|
|
6
|
+
export * from './fold.js';
|
|
7
|
+
export * from './store.js';
|
|
8
|
+
export * from './observation.js';
|
|
9
|
+
export * from './service.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-406 mission control runtime (M0 contract + storage, M1 observation).
|
|
3
|
+
*/
|
|
4
|
+
export * from './schemas.js';
|
|
5
|
+
export * from './transitions.js';
|
|
6
|
+
export * from './fold.js';
|
|
7
|
+
export * from './store.js';
|
|
8
|
+
export * from './observation.js';
|
|
9
|
+
export * from './service.js';
|
|
10
|
+
//# sourceMappingURL=index.js.map
|