claude-flow 3.50.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/proven-config.json +1 -1
- package/.claude/settings.json +11 -2
- package/.claude-plugin/marketplace.json +5 -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 +8 -3
- package/v3/@claude-flow/cli/dist/src/commands/index.js +3 -0
- package/v3/@claude-flow/cli/dist/src/commands/init.js +50 -11
- 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 +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/mods.js +67 -23
- 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/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 +60 -22
- package/v3/@claude-flow/cli/dist/src/mods/install.js +91 -41
- 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/probe.d.ts +5 -0
- package/v3/@claude-flow/cli/dist/src/mods/probe.js +28 -3
- 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 +5 -1
- package/v3/@claude-flow/cli/package.json +1 -1
|
@@ -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
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-406 M1 / §11 — the bounded mission observation file a workbench reads.
|
|
3
|
+
*
|
|
4
|
+
* `.claude-flow/missions/observation.json` is rewritten atomically after each
|
|
5
|
+
* committed mission event. It carries the source time (`observedAt`, the time
|
|
6
|
+
* of the newest committed event); readers stamp their own fetch time and derive
|
|
7
|
+
* freshness from `observedAt`, never from a redraw. Strings are stripped of
|
|
8
|
+
* control and bidi characters and truncated here, at write time, so a reader
|
|
9
|
+
* never sees unbounded or instruction-shaped text from a mission.
|
|
10
|
+
*
|
|
11
|
+
* It is observation only. Task status is recorded state; only
|
|
12
|
+
* `evidence.verified` counts as verified, and an executor marked disconnected
|
|
13
|
+
* is not a failed mission.
|
|
14
|
+
*/
|
|
15
|
+
import type { MissionView } from './transitions.js';
|
|
16
|
+
export declare const OBSERVATION_CONTRACT: "ruflo.mission-observation/1";
|
|
17
|
+
export declare const OBSERVATION_FILE = "observation.json";
|
|
18
|
+
export declare const OBSERVATION_LIMITS: Readonly<{
|
|
19
|
+
missions: 50;
|
|
20
|
+
tasks: 50;
|
|
21
|
+
string: 200;
|
|
22
|
+
objective: 400;
|
|
23
|
+
}>;
|
|
24
|
+
export interface MissionObservation {
|
|
25
|
+
readonly schemaVersion: 1;
|
|
26
|
+
readonly contract: typeof OBSERVATION_CONTRACT;
|
|
27
|
+
readonly source: 'ruflo-cli/missions';
|
|
28
|
+
readonly workspaceId: string;
|
|
29
|
+
readonly observedAt: string | null;
|
|
30
|
+
readonly truncated: boolean;
|
|
31
|
+
readonly missions: readonly ObservedMission[];
|
|
32
|
+
}
|
|
33
|
+
export interface ObservedMission {
|
|
34
|
+
readonly missionId: string;
|
|
35
|
+
readonly objective: string;
|
|
36
|
+
readonly state: string;
|
|
37
|
+
readonly revision: number;
|
|
38
|
+
readonly executionMode: string;
|
|
39
|
+
readonly plan: {
|
|
40
|
+
readonly revision: number;
|
|
41
|
+
readonly digest: string;
|
|
42
|
+
readonly taskCount: number;
|
|
43
|
+
readonly tasks: readonly {
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly title: string;
|
|
46
|
+
readonly dependsOn: readonly string[];
|
|
47
|
+
readonly status: string;
|
|
48
|
+
}[];
|
|
49
|
+
};
|
|
50
|
+
readonly budget: MissionView['budget'];
|
|
51
|
+
readonly evidence: {
|
|
52
|
+
readonly count: number;
|
|
53
|
+
readonly verified: number;
|
|
54
|
+
};
|
|
55
|
+
readonly executor: {
|
|
56
|
+
readonly connection: string;
|
|
57
|
+
readonly observedAt: string;
|
|
58
|
+
readonly lastAckExecutionId: string | null;
|
|
59
|
+
} | null;
|
|
60
|
+
readonly unresolvedOperations: number;
|
|
61
|
+
readonly blockedReason: string | null;
|
|
62
|
+
readonly lastEventSequence: number;
|
|
63
|
+
readonly updatedAt: string;
|
|
64
|
+
}
|
|
65
|
+
/** Strip C0/C1 controls and bidi overrides, then bound the length. */
|
|
66
|
+
export declare function sanitize(value: string, max?: number): string;
|
|
67
|
+
export declare function observeMission(view: MissionView): ObservedMission;
|
|
68
|
+
/** Newest-updated first, capped; `observedAt` is the newest committed event time. */
|
|
69
|
+
export declare function buildObservation(workspaceId: string, views: readonly MissionView[]): MissionObservation;
|
|
70
|
+
//# sourceMappingURL=observation.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-406 M1 / §11 — the bounded mission observation file a workbench reads.
|
|
3
|
+
*
|
|
4
|
+
* `.claude-flow/missions/observation.json` is rewritten atomically after each
|
|
5
|
+
* committed mission event. It carries the source time (`observedAt`, the time
|
|
6
|
+
* of the newest committed event); readers stamp their own fetch time and derive
|
|
7
|
+
* freshness from `observedAt`, never from a redraw. Strings are stripped of
|
|
8
|
+
* control and bidi characters and truncated here, at write time, so a reader
|
|
9
|
+
* never sees unbounded or instruction-shaped text from a mission.
|
|
10
|
+
*
|
|
11
|
+
* It is observation only. Task status is recorded state; only
|
|
12
|
+
* `evidence.verified` counts as verified, and an executor marked disconnected
|
|
13
|
+
* is not a failed mission.
|
|
14
|
+
*/
|
|
15
|
+
export const OBSERVATION_CONTRACT = 'ruflo.mission-observation/1';
|
|
16
|
+
export const OBSERVATION_FILE = 'observation.json';
|
|
17
|
+
export const OBSERVATION_LIMITS = Object.freeze({ missions: 50, tasks: 50, string: 200, objective: 400 });
|
|
18
|
+
/** Strip C0/C1 controls and bidi overrides, then bound the length. */
|
|
19
|
+
export function sanitize(value, max = OBSERVATION_LIMITS.string) {
|
|
20
|
+
let out = '';
|
|
21
|
+
for (const ch of value) {
|
|
22
|
+
const cp = ch.codePointAt(0);
|
|
23
|
+
if (cp < 0x20 || cp === 0x7f || (cp >= 0x80 && cp <= 0x9f)) {
|
|
24
|
+
out += ' ';
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if ((cp >= 0x202a && cp <= 0x202e) || (cp >= 0x2066 && cp <= 0x2069) || cp === 0x200e || cp === 0x200f)
|
|
28
|
+
continue;
|
|
29
|
+
out += ch;
|
|
30
|
+
}
|
|
31
|
+
out = out.replace(/\s+/g, ' ').trim();
|
|
32
|
+
return out.length > max ? `${out.slice(0, max - 1)}…` : out;
|
|
33
|
+
}
|
|
34
|
+
export function observeMission(view) {
|
|
35
|
+
const plan = view.planBody;
|
|
36
|
+
return {
|
|
37
|
+
missionId: view.record.missionId,
|
|
38
|
+
objective: sanitize(view.record.objective, OBSERVATION_LIMITS.objective),
|
|
39
|
+
state: view.record.state,
|
|
40
|
+
revision: view.record.revision,
|
|
41
|
+
executionMode: view.record.executionMode,
|
|
42
|
+
plan: {
|
|
43
|
+
revision: view.record.plan.revision,
|
|
44
|
+
digest: view.record.plan.digest,
|
|
45
|
+
taskCount: plan?.tasks.length ?? 0,
|
|
46
|
+
tasks: (plan?.tasks ?? []).slice(0, OBSERVATION_LIMITS.tasks).map((t) => ({
|
|
47
|
+
id: sanitize(t.id, 64),
|
|
48
|
+
title: sanitize(t.title),
|
|
49
|
+
dependsOn: t.dependsOn.slice(0, 16).map((d) => sanitize(d, 64)),
|
|
50
|
+
status: view.tasks[t.id]?.status ?? 'pending',
|
|
51
|
+
})),
|
|
52
|
+
},
|
|
53
|
+
budget: view.budget,
|
|
54
|
+
evidence: { count: view.evidence.length, verified: view.evidence.filter((e) => e.verified).length },
|
|
55
|
+
executor: view.executor
|
|
56
|
+
? { connection: view.executor.connection, observedAt: view.executor.observedAt, lastAckExecutionId: view.executor.lastAckExecutionId }
|
|
57
|
+
: null,
|
|
58
|
+
unresolvedOperations: view.unresolvedOperations.length,
|
|
59
|
+
blockedReason: view.blockedReason ? sanitize(view.blockedReason) : null,
|
|
60
|
+
lastEventSequence: view.record.lastEventSequence,
|
|
61
|
+
updatedAt: view.record.updatedAt,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Newest-updated first, capped; `observedAt` is the newest committed event time. */
|
|
65
|
+
export function buildObservation(workspaceId, views) {
|
|
66
|
+
const sorted = [...views].sort((a, b) => (a.record.updatedAt < b.record.updatedAt ? 1 : a.record.updatedAt > b.record.updatedAt ? -1 : 0));
|
|
67
|
+
return {
|
|
68
|
+
schemaVersion: 1,
|
|
69
|
+
contract: OBSERVATION_CONTRACT,
|
|
70
|
+
source: 'ruflo-cli/missions',
|
|
71
|
+
workspaceId,
|
|
72
|
+
observedAt: sorted[0]?.record.updatedAt ?? null,
|
|
73
|
+
truncated: sorted.length > OBSERVATION_LIMITS.missions,
|
|
74
|
+
missions: sorted.slice(0, OBSERVATION_LIMITS.missions).map(observeMission),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=observation.js.map
|