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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-406 §19.3 — versioned mission schemas.
|
|
3
|
+
*
|
|
4
|
+
* `MissionRecord` is the §19.3 logical schema field for field (strict). The
|
|
5
|
+
* plan body, budget ledger, evidence index and executor observations are kept
|
|
6
|
+
* in a `MissionView` beside it, so the record itself stays exactly the ADR's.
|
|
7
|
+
*
|
|
8
|
+
* Every client input (CLI and MCP alike) is parsed here: strict objects,
|
|
9
|
+
* bounded strings and arrays, integer monetary units. Principal, tenant and
|
|
10
|
+
* workspace are never accepted from input; the service derives them.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
export const MISSION_SCHEMA_VERSION = 1;
|
|
14
|
+
export const MISSION_EVENT_CONTRACT = 'ruflo.mission-event/1';
|
|
15
|
+
export const MISSION_LIMITS = Object.freeze({
|
|
16
|
+
objective: 2_000,
|
|
17
|
+
title: 200,
|
|
18
|
+
text: 500,
|
|
19
|
+
id: 64,
|
|
20
|
+
tasks: 100,
|
|
21
|
+
criteria: 50,
|
|
22
|
+
deps: 32,
|
|
23
|
+
capabilities: 32,
|
|
24
|
+
maxMinor: 1_000_000_000_000, // 10^12 minor units: far above any sane ceiling, below 2^53
|
|
25
|
+
requestId: 128,
|
|
26
|
+
eventsPage: 500,
|
|
27
|
+
});
|
|
28
|
+
export const missionStateSchema = z.enum([
|
|
29
|
+
'draft', 'planned', 'awaitingAuthorization', 'queued',
|
|
30
|
+
'running', 'pauseRequested', 'paused', 'blocked',
|
|
31
|
+
'verifying', 'completed', 'failed',
|
|
32
|
+
'cancelRequested', 'cancelled',
|
|
33
|
+
]);
|
|
34
|
+
export const TERMINAL_STATES = new Set(['completed', 'failed', 'cancelled']);
|
|
35
|
+
export const executionModeSchema = z.enum(['durable-executor', 'session-bound']);
|
|
36
|
+
export const MISSION_ID = /^msn_[a-f0-9]{24}$/;
|
|
37
|
+
const SHORT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
38
|
+
const DIGEST = /^(?:sha256:[a-f0-9]{64}|none)$/;
|
|
39
|
+
const text = (max) => z.string().min(1).max(max);
|
|
40
|
+
const shortId = z.string().regex(SHORT_ID);
|
|
41
|
+
const minor = z.number().int().min(0).max(MISSION_LIMITS.maxMinor);
|
|
42
|
+
/** §19.3, exactly. */
|
|
43
|
+
export const missionRecordSchema = z.object({
|
|
44
|
+
schemaVersion: z.literal(MISSION_SCHEMA_VERSION),
|
|
45
|
+
missionId: z.string().regex(MISSION_ID),
|
|
46
|
+
tenantId: text(MISSION_LIMITS.id),
|
|
47
|
+
workspaceId: text(MISSION_LIMITS.id),
|
|
48
|
+
ownerPrincipalId: text(MISSION_LIMITS.title),
|
|
49
|
+
revision: z.number().int().min(1),
|
|
50
|
+
objective: text(MISSION_LIMITS.objective),
|
|
51
|
+
plan: z.object({
|
|
52
|
+
revision: z.number().int().min(0),
|
|
53
|
+
digest: z.string().regex(DIGEST),
|
|
54
|
+
taskGraphRef: z.string().max(MISSION_LIMITS.title),
|
|
55
|
+
}).strict(),
|
|
56
|
+
policyRef: text(MISSION_LIMITS.title),
|
|
57
|
+
authorizationRef: text(MISSION_LIMITS.title).optional(),
|
|
58
|
+
budgetRef: text(MISSION_LIMITS.title),
|
|
59
|
+
executionMode: executionModeSchema,
|
|
60
|
+
state: missionStateSchema,
|
|
61
|
+
evidenceRefs: z.array(text(MISSION_LIMITS.title)),
|
|
62
|
+
acceptance: z.object({ revision: z.number().int().min(0), criteriaDigest: z.string().regex(DIGEST) }).strict(),
|
|
63
|
+
lastEventSequence: z.number().int().min(1),
|
|
64
|
+
createdAt: z.string().datetime(),
|
|
65
|
+
updatedAt: z.string().datetime(),
|
|
66
|
+
}).strict();
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Plan input
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
export const taskSpecSchema = z.object({
|
|
71
|
+
id: shortId,
|
|
72
|
+
title: text(MISSION_LIMITS.title),
|
|
73
|
+
dependsOn: z.array(shortId).max(MISSION_LIMITS.deps).default([]),
|
|
74
|
+
executor: z.object({
|
|
75
|
+
mode: executionModeSchema,
|
|
76
|
+
requirement: text(MISSION_LIMITS.title),
|
|
77
|
+
}).strict(),
|
|
78
|
+
capabilityCeiling: z.array(text(MISSION_LIMITS.title)).max(MISSION_LIMITS.capabilities).default([]),
|
|
79
|
+
estimatedCostMinor: minor.default(0),
|
|
80
|
+
checkpoint: text(MISSION_LIMITS.title).optional(),
|
|
81
|
+
acceptanceEvidence: z.array(shortId).max(MISSION_LIMITS.criteria).default([]),
|
|
82
|
+
}).strict();
|
|
83
|
+
export const acceptanceCriterionSchema = z.object({
|
|
84
|
+
id: shortId,
|
|
85
|
+
check: text(MISSION_LIMITS.text),
|
|
86
|
+
inputs: z.array(text(MISSION_LIMITS.title)).max(16).default([]),
|
|
87
|
+
baseline: text(MISSION_LIMITS.title).optional(),
|
|
88
|
+
threshold: text(MISSION_LIMITS.title).optional(),
|
|
89
|
+
producer: text(MISSION_LIMITS.title),
|
|
90
|
+
independent: z.boolean().default(false),
|
|
91
|
+
mandatory: z.boolean().default(true),
|
|
92
|
+
}).strict();
|
|
93
|
+
export const budgetSpecSchema = z.object({
|
|
94
|
+
currency: z.string().regex(/^[A-Z]{3}$/),
|
|
95
|
+
ceilingMinor: minor.refine((v) => v > 0, 'ceilingMinor must be positive'),
|
|
96
|
+
tokens: z.number().int().min(0).optional(),
|
|
97
|
+
wallTimeSeconds: z.number().int().min(0).optional(),
|
|
98
|
+
concurrency: z.number().int().min(1).max(64).optional(),
|
|
99
|
+
retries: z.number().int().min(0).max(100).optional(),
|
|
100
|
+
}).strict();
|
|
101
|
+
export const planBodySchema = z.object({
|
|
102
|
+
tasks: z.array(taskSpecSchema).min(1).max(MISSION_LIMITS.tasks),
|
|
103
|
+
acceptance: z.array(acceptanceCriterionSchema).min(1).max(MISSION_LIMITS.criteria),
|
|
104
|
+
budget: budgetSpecSchema,
|
|
105
|
+
scope: z.object({
|
|
106
|
+
capabilities: z.array(text(MISSION_LIMITS.title)).max(MISSION_LIMITS.capabilities).default([]),
|
|
107
|
+
}).strict().default({ capabilities: [] }),
|
|
108
|
+
}).strict();
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Operation inputs (shared by CLI and MCP)
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
const requestId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
|
|
113
|
+
const missionId = z.string().regex(MISSION_ID);
|
|
114
|
+
export const createInputSchema = z.object({
|
|
115
|
+
requestId,
|
|
116
|
+
objective: text(MISSION_LIMITS.objective),
|
|
117
|
+
}).strict();
|
|
118
|
+
export const planInputSchema = z.object({
|
|
119
|
+
requestId,
|
|
120
|
+
missionId,
|
|
121
|
+
expectedRevision: z.number().int().min(1),
|
|
122
|
+
plan: planBodySchema,
|
|
123
|
+
}).strict();
|
|
124
|
+
export const getInputSchema = z.object({ missionId: missionId.optional() }).strict();
|
|
125
|
+
export const eventsInputSchema = z.object({
|
|
126
|
+
missionId,
|
|
127
|
+
afterSequence: z.number().int().min(0).default(0),
|
|
128
|
+
limit: z.number().int().min(1).max(MISSION_LIMITS.eventsPage).default(100),
|
|
129
|
+
}).strict();
|
|
130
|
+
export const requestActionInputSchema = z.object({
|
|
131
|
+
requestId,
|
|
132
|
+
missionId,
|
|
133
|
+
expectedRevision: z.number().int().min(1),
|
|
134
|
+
action: z.enum(['pause', 'resume', 'cancel', 'requestAuthorization', 'admit']),
|
|
135
|
+
reason: text(MISSION_LIMITS.text).optional(),
|
|
136
|
+
}).strict();
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Events
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
/** Control events change mission state or authority and bump `revision`. */
|
|
141
|
+
export const CONTROL_EVENTS = [
|
|
142
|
+
'mission.created', 'plan.validated', 'plan.revised', 'authorization.missing', 'admission.accepted',
|
|
143
|
+
'executor.acknowledged', 'pause.accepted', 'quiescence.confirmed', 'blocked', 'resume.admitted',
|
|
144
|
+
'tasks.settled', 'acceptance.passed', 'acceptance.failed', 'cancel.admitted', 'cancel.settled',
|
|
145
|
+
'failure.verified',
|
|
146
|
+
];
|
|
147
|
+
/** Observations record what was seen; they never change state or revision. */
|
|
148
|
+
export const OBSERVATION_EVENTS = ['executor.observed', 'evidence.recorded', 'task.observed'];
|
|
149
|
+
export const executorConnectionSchema = z.enum(['healthy', 'stale', 'disconnected', 'unknown']);
|
|
150
|
+
//# sourceMappingURL=schemas.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-406 §19.7 — the mission semantic operations, shared by every client.
|
|
3
|
+
*
|
|
4
|
+
* `mission.create`, `mission.plan`, `mission.get`, `mission.events` and
|
|
5
|
+
* `mission.requestAction` parse raw input with the same zod schemas whether
|
|
6
|
+
* the caller is the CLI or an MCP tool. Principal, tenant and workspace are
|
|
7
|
+
* derived from the trusted local context (the OS user owning this process and
|
|
8
|
+
* the canonical project path); nothing in the input can name them, and no
|
|
9
|
+
* input field is treated as proof of permission.
|
|
10
|
+
*
|
|
11
|
+
* Every mutation carries a request id and the expected mission revision. A
|
|
12
|
+
* stale revision is refused with the current one (refreshable conflict). A
|
|
13
|
+
* repeated request id with the same input returns the original outcome; with
|
|
14
|
+
* different input it is refused.
|
|
15
|
+
*/
|
|
16
|
+
import { type MissionEvent } from './schemas.js';
|
|
17
|
+
export type Channel = 'cli' | 'mcp';
|
|
18
|
+
export interface MissionContext {
|
|
19
|
+
readonly projectRoot: string;
|
|
20
|
+
readonly channel: Channel;
|
|
21
|
+
readonly now?: () => Date;
|
|
22
|
+
}
|
|
23
|
+
export type MissionResult<T> = {
|
|
24
|
+
readonly ok: true;
|
|
25
|
+
readonly data: T;
|
|
26
|
+
} | {
|
|
27
|
+
readonly ok: false;
|
|
28
|
+
readonly code: string;
|
|
29
|
+
readonly message: string;
|
|
30
|
+
readonly currentRevision?: number;
|
|
31
|
+
};
|
|
32
|
+
export declare class RevisionConflict extends Error {
|
|
33
|
+
readonly currentRevision: number;
|
|
34
|
+
constructor(currentRevision: number, expected: number);
|
|
35
|
+
}
|
|
36
|
+
export declare class MissionService {
|
|
37
|
+
private readonly ctx;
|
|
38
|
+
private readonly store;
|
|
39
|
+
private readonly identity;
|
|
40
|
+
constructor(ctx: MissionContext);
|
|
41
|
+
private now;
|
|
42
|
+
private event;
|
|
43
|
+
/** Same principal + request id earlier in this mission's log: same input replays, other input is refused. */
|
|
44
|
+
private prior;
|
|
45
|
+
private publishObservation;
|
|
46
|
+
create(raw: unknown): Promise<MissionResult<{
|
|
47
|
+
missionId: string;
|
|
48
|
+
revision: number;
|
|
49
|
+
state: string;
|
|
50
|
+
deduplicated: boolean;
|
|
51
|
+
}>>;
|
|
52
|
+
/** Run a control transition with request dedup and an expected-revision check. */
|
|
53
|
+
private control;
|
|
54
|
+
plan(raw: unknown): Promise<MissionResult<{
|
|
55
|
+
missionId: string;
|
|
56
|
+
revision: number;
|
|
57
|
+
state: string;
|
|
58
|
+
deduplicated: boolean;
|
|
59
|
+
sequence: number;
|
|
60
|
+
planDigest?: string;
|
|
61
|
+
}>>;
|
|
62
|
+
/**
|
|
63
|
+
* Scoped control requests. Admission and resume need an admitted executor
|
|
64
|
+
* and a policy-authorized budget reservation (M2); until one exists they are
|
|
65
|
+
* refused with an explicit reason rather than recorded as if work started.
|
|
66
|
+
*/
|
|
67
|
+
requestAction(raw: unknown): Promise<MissionResult<{
|
|
68
|
+
missionId: string;
|
|
69
|
+
revision: number;
|
|
70
|
+
state: string;
|
|
71
|
+
deduplicated: boolean;
|
|
72
|
+
sequence: number;
|
|
73
|
+
}>>;
|
|
74
|
+
get(raw?: unknown): Promise<MissionResult<unknown>>;
|
|
75
|
+
/**
|
|
76
|
+
* Events after a cursor. Delivery may repeat; clients deduplicate by
|
|
77
|
+
* (missionId, seq). `retainedFromSequence` is the oldest event kept; a cursor
|
|
78
|
+
* older than that gets `gap: true` and must reload a snapshot first.
|
|
79
|
+
*/
|
|
80
|
+
events(raw: unknown): Promise<MissionResult<{
|
|
81
|
+
events: MissionEvent[];
|
|
82
|
+
nextCursor: number;
|
|
83
|
+
lastEventSequence: number;
|
|
84
|
+
retainedFromSequence: number;
|
|
85
|
+
gap: boolean;
|
|
86
|
+
}>>;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-406 §19.7 — the mission semantic operations, shared by every client.
|
|
3
|
+
*
|
|
4
|
+
* `mission.create`, `mission.plan`, `mission.get`, `mission.events` and
|
|
5
|
+
* `mission.requestAction` parse raw input with the same zod schemas whether
|
|
6
|
+
* the caller is the CLI or an MCP tool. Principal, tenant and workspace are
|
|
7
|
+
* derived from the trusted local context (the OS user owning this process and
|
|
8
|
+
* the canonical project path); nothing in the input can name them, and no
|
|
9
|
+
* input field is treated as proof of permission.
|
|
10
|
+
*
|
|
11
|
+
* Every mutation carries a request id and the expected mission revision. A
|
|
12
|
+
* stale revision is refused with the current one (refreshable conflict). A
|
|
13
|
+
* repeated request id with the same input returns the original outcome; with
|
|
14
|
+
* different input it is refused.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { realpathSync } from 'node:fs';
|
|
18
|
+
import { userInfo } from 'node:os';
|
|
19
|
+
import { resolve } from 'node:path';
|
|
20
|
+
import { MISSION_EVENT_CONTRACT, createInputSchema, eventsInputSchema, getInputSchema, planInputSchema, requestActionInputSchema, } from './schemas.js';
|
|
21
|
+
import { TransitionError, checkTransition, digestOf, validatePlan } from './transitions.js';
|
|
22
|
+
import { eventHash, foldEvent, GENESIS_HASH } from './fold.js';
|
|
23
|
+
import { MissionStore, MissionStoreError } from './store.js';
|
|
24
|
+
import { OBSERVATION_FILE, buildObservation, observeMission } from './observation.js';
|
|
25
|
+
export class RevisionConflict extends Error {
|
|
26
|
+
currentRevision;
|
|
27
|
+
constructor(currentRevision, expected) {
|
|
28
|
+
super(`revision-conflict: expected ${expected}, current ${currentRevision}; refresh and retry`);
|
|
29
|
+
this.currentRevision = currentRevision;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
class RequestReuse extends Error {
|
|
33
|
+
constructor(requestId) { super(`request-id-reuse: ${requestId} was already used with different input`); }
|
|
34
|
+
}
|
|
35
|
+
function identityOf(ctx) {
|
|
36
|
+
let root;
|
|
37
|
+
try {
|
|
38
|
+
root = realpathSync(resolve(ctx.projectRoot));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
root = resolve(ctx.projectRoot);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
principalId: `local:${userInfo().username}`,
|
|
45
|
+
tenantId: 'local',
|
|
46
|
+
workspaceId: `ws_${createHash('sha256').update(root).digest('hex').slice(0, 16)}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function parse(schema, raw) {
|
|
50
|
+
const r = schema.safeParse(raw);
|
|
51
|
+
if (!r.success)
|
|
52
|
+
throw new TransitionError('invalid-input', r.error.issues.map((i) => `${i.path.join('.') || 'input'}: ${i.message}`).join('; '));
|
|
53
|
+
return r.data;
|
|
54
|
+
}
|
|
55
|
+
function fail(error) {
|
|
56
|
+
if (error instanceof RevisionConflict)
|
|
57
|
+
return { ok: false, code: 'revision-conflict', message: error.message, currentRevision: error.currentRevision };
|
|
58
|
+
if (error instanceof TransitionError || error instanceof MissionStoreError)
|
|
59
|
+
return { ok: false, code: error.code, message: error.message };
|
|
60
|
+
if (error instanceof RequestReuse)
|
|
61
|
+
return { ok: false, code: 'request-id-reuse', message: error.message };
|
|
62
|
+
return { ok: false, code: 'internal', message: error instanceof Error ? error.message : String(error) };
|
|
63
|
+
}
|
|
64
|
+
export class MissionService {
|
|
65
|
+
ctx;
|
|
66
|
+
store;
|
|
67
|
+
identity;
|
|
68
|
+
constructor(ctx) {
|
|
69
|
+
this.ctx = ctx;
|
|
70
|
+
this.store = new MissionStore(ctx.projectRoot);
|
|
71
|
+
this.identity = identityOf(ctx);
|
|
72
|
+
}
|
|
73
|
+
now() { return (this.ctx.now ?? (() => new Date()))().toISOString(); }
|
|
74
|
+
event(view, missionId, type, payload, request, control) {
|
|
75
|
+
const draft = {
|
|
76
|
+
contract: MISSION_EVENT_CONTRACT,
|
|
77
|
+
seq: view ? view.record.lastEventSequence + 1 : 1,
|
|
78
|
+
missionId,
|
|
79
|
+
type,
|
|
80
|
+
expectedRevision: request?.expectedRevision ?? null,
|
|
81
|
+
revision: view ? view.record.revision + (control ? 1 : 0) : 1,
|
|
82
|
+
principalId: this.identity.principalId,
|
|
83
|
+
channel: this.ctx.channel,
|
|
84
|
+
requestId: request?.requestId ?? null,
|
|
85
|
+
requestDigest: request?.digest ?? null,
|
|
86
|
+
policyDecisionRef: null,
|
|
87
|
+
at: this.now(),
|
|
88
|
+
payload,
|
|
89
|
+
prevHash: view ? view.lastHash : GENESIS_HASH,
|
|
90
|
+
};
|
|
91
|
+
return { ...draft, hash: eventHash(draft) };
|
|
92
|
+
}
|
|
93
|
+
/** Same principal + request id earlier in this mission's log: same input replays, other input is refused. */
|
|
94
|
+
prior(events, requestId, digest) {
|
|
95
|
+
const hit = events.find((e) => e.requestId === requestId && e.principalId === this.identity.principalId);
|
|
96
|
+
if (!hit)
|
|
97
|
+
return null;
|
|
98
|
+
if (hit.requestDigest !== digest)
|
|
99
|
+
throw new RequestReuse(requestId);
|
|
100
|
+
return hit;
|
|
101
|
+
}
|
|
102
|
+
async publishObservation() {
|
|
103
|
+
await this.store.withWorkspaceLock(() => {
|
|
104
|
+
const views = this.store.listMissionIds().map((id) => this.store.loadView(id)).filter((v) => v !== null);
|
|
105
|
+
this.store.writeWorkspaceFile(OBSERVATION_FILE, `${JSON.stringify(buildObservation(this.identity.workspaceId, views), null, 2)}\n`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async create(raw) {
|
|
109
|
+
try {
|
|
110
|
+
const input = parse(createInputSchema, raw);
|
|
111
|
+
const digest = digestOf({ op: 'mission.create', input });
|
|
112
|
+
const scope = [this.identity.tenantId, this.identity.principalId, this.identity.workspaceId, 'mission.create', input.requestId].join('\u0000');
|
|
113
|
+
const missionId = `msn_${createHash('sha256').update(scope).digest('hex').slice(0, 24)}`;
|
|
114
|
+
this.store.claim(missionId); // atomic; a concurrent duplicate falls through to the dedup check below
|
|
115
|
+
const result = await this.store.withMission(missionId, (view, log) => {
|
|
116
|
+
if (view) {
|
|
117
|
+
this.prior(log.events, input.requestId, digest);
|
|
118
|
+
return { events: [], view, result: { missionId, revision: view.record.revision, state: view.record.state, deduplicated: true } };
|
|
119
|
+
}
|
|
120
|
+
const event = this.event(null, missionId, 'mission.created', {
|
|
121
|
+
objective: input.objective,
|
|
122
|
+
tenantId: this.identity.tenantId,
|
|
123
|
+
workspaceId: this.identity.workspaceId,
|
|
124
|
+
ownerPrincipalId: this.identity.principalId,
|
|
125
|
+
policyRef: 'policy:.claude-flow/policy/state.json',
|
|
126
|
+
budgetRef: `budget:${missionId}`,
|
|
127
|
+
}, { requestId: input.requestId, digest, expectedRevision: null }, true);
|
|
128
|
+
const next = foldEvent(null, event);
|
|
129
|
+
return { events: [event], view: next, result: { missionId, revision: next.record.revision, state: next.record.state, deduplicated: false } };
|
|
130
|
+
});
|
|
131
|
+
if (!result.deduplicated)
|
|
132
|
+
await this.publishObservation();
|
|
133
|
+
return { ok: true, data: result };
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return fail(error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Run a control transition with request dedup and an expected-revision check. */
|
|
140
|
+
async control(missionId, requestId, expectedRevision, digest, decide) {
|
|
141
|
+
try {
|
|
142
|
+
if (!this.store.exists(missionId))
|
|
143
|
+
throw new MissionStoreError('not-found', missionId);
|
|
144
|
+
const result = await this.store.withMission(missionId, (view, log) => {
|
|
145
|
+
if (!view)
|
|
146
|
+
throw new MissionStoreError('not-found', missionId);
|
|
147
|
+
const prior = this.prior(log.events, requestId, digest);
|
|
148
|
+
if (prior) {
|
|
149
|
+
return { events: [], view, result: { missionId, revision: prior.revision, state: view.record.state, deduplicated: true, sequence: prior.seq } };
|
|
150
|
+
}
|
|
151
|
+
if (expectedRevision !== view.record.revision)
|
|
152
|
+
throw new RevisionConflict(view.record.revision, expectedRevision);
|
|
153
|
+
const { type, payload, then } = decide(view);
|
|
154
|
+
checkTransition(view, type, payload);
|
|
155
|
+
const event = this.event(view, missionId, type, payload, { requestId, digest, expectedRevision }, true);
|
|
156
|
+
let next = foldEvent(view, event);
|
|
157
|
+
const events = [event];
|
|
158
|
+
const follow = then?.(next);
|
|
159
|
+
if (follow) {
|
|
160
|
+
// A consequence decided by the runtime in the same transaction, not a client request.
|
|
161
|
+
const second = this.event(next, missionId, follow.type, follow.payload, { requestId: `${requestId}#settle`, digest, expectedRevision: next.record.revision }, true);
|
|
162
|
+
next = foldEvent(next, second);
|
|
163
|
+
events.push(second);
|
|
164
|
+
}
|
|
165
|
+
const lastSeq = events[events.length - 1].seq;
|
|
166
|
+
return { events, view: next, result: { missionId, revision: next.record.revision, state: next.record.state, deduplicated: false, sequence: lastSeq } };
|
|
167
|
+
});
|
|
168
|
+
if (!result.deduplicated)
|
|
169
|
+
await this.publishObservation();
|
|
170
|
+
return { ok: true, data: result };
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
return fail(error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async plan(raw) {
|
|
177
|
+
let input;
|
|
178
|
+
try {
|
|
179
|
+
input = parse(planInputSchema, raw);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return fail(error);
|
|
183
|
+
}
|
|
184
|
+
const checked = validatePlan(input.plan);
|
|
185
|
+
if (checked.error !== null)
|
|
186
|
+
return { ok: false, code: 'invalid-plan', message: checked.error };
|
|
187
|
+
const planDigest = digestOf(checked.plan);
|
|
188
|
+
const result = await this.control(input.missionId, input.requestId, input.expectedRevision, digestOf({ op: 'mission.plan', missionId: input.missionId, plan: checked.plan }), (view) => ({ type: view.record.state === 'draft' ? 'plan.validated' : 'plan.revised', payload: { plan: checked.plan, digest: planDigest } }));
|
|
189
|
+
return result.ok ? { ok: true, data: { ...result.data, planDigest } } : result;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Scoped control requests. Admission and resume need an admitted executor
|
|
193
|
+
* and a policy-authorized budget reservation (M2); until one exists they are
|
|
194
|
+
* refused with an explicit reason rather than recorded as if work started.
|
|
195
|
+
*/
|
|
196
|
+
async requestAction(raw) {
|
|
197
|
+
let input;
|
|
198
|
+
try {
|
|
199
|
+
input = parse(requestActionInputSchema, raw);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
return fail(error);
|
|
203
|
+
}
|
|
204
|
+
if (input.action === 'admit' || input.action === 'resume') {
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
code: 'executor-unavailable',
|
|
208
|
+
message: 'no durable executor independent of the Claude process is admitted yet; admission and resume are not available (ADR-406 M2)',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const digest = digestOf({ op: 'mission.requestAction', input });
|
|
212
|
+
return this.control(input.missionId, input.requestId, input.expectedRevision, digest, (view) => {
|
|
213
|
+
if (input.action === 'requestAuthorization')
|
|
214
|
+
return { type: 'authorization.missing', payload: { reason: input.reason ?? 'authorization requested for review' } };
|
|
215
|
+
if (input.action === 'pause')
|
|
216
|
+
return { type: 'pause.accepted', payload: { reason: input.reason ?? null } };
|
|
217
|
+
// Nothing was ever admitted to an executor, so no executor holds work to
|
|
218
|
+
// acknowledge: settlement is immediate. Otherwise the mission stays
|
|
219
|
+
// cancelRequested until the executor confirms (§9, §19.4).
|
|
220
|
+
const neverAdmitted = view.lease.epoch === 0 && view.lease.executorId === null;
|
|
221
|
+
return {
|
|
222
|
+
type: 'cancel.admitted',
|
|
223
|
+
payload: { reason: input.reason ?? null },
|
|
224
|
+
then: neverAdmitted ? () => ({ type: 'cancel.settled', payload: { acknowledgedExecutors: [], settledBy: 'no-executor-admitted' } }) : undefined,
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async get(raw = {}) {
|
|
229
|
+
try {
|
|
230
|
+
const input = parse(getInputSchema, raw ?? {});
|
|
231
|
+
if (input.missionId) {
|
|
232
|
+
const view = this.store.exists(input.missionId) ? this.store.loadView(input.missionId) : null;
|
|
233
|
+
if (!view)
|
|
234
|
+
throw new MissionStoreError('not-found', input.missionId);
|
|
235
|
+
return { ok: true, data: { record: view.record, plan: view.planBody, budget: view.budget, tasks: view.tasks, evidence: view.evidence, executor: view.executor, unresolvedOperations: view.unresolvedOperations, blockedReason: view.blockedReason } };
|
|
236
|
+
}
|
|
237
|
+
const missions = this.store.listMissionIds().map((id) => this.store.loadView(id)).filter((v) => v !== null).map(observeMission);
|
|
238
|
+
return { ok: true, data: { workspaceId: this.identity.workspaceId, missions } };
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
return fail(error);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Events after a cursor. Delivery may repeat; clients deduplicate by
|
|
246
|
+
* (missionId, seq). `retainedFromSequence` is the oldest event kept; a cursor
|
|
247
|
+
* older than that gets `gap: true` and must reload a snapshot first.
|
|
248
|
+
*/
|
|
249
|
+
async events(raw) {
|
|
250
|
+
try {
|
|
251
|
+
const input = parse(eventsInputSchema, raw);
|
|
252
|
+
if (!this.store.exists(input.missionId))
|
|
253
|
+
throw new MissionStoreError('not-found', input.missionId);
|
|
254
|
+
const log = this.store.readLog(input.missionId);
|
|
255
|
+
// Validate the whole chain before serving any of it.
|
|
256
|
+
let view = null;
|
|
257
|
+
for (const e of log.events)
|
|
258
|
+
view = foldEvent(view, e);
|
|
259
|
+
const retainedFromSequence = 1;
|
|
260
|
+
const page = log.events.filter((e) => e.seq > input.afterSequence).slice(0, input.limit);
|
|
261
|
+
const last = log.events.length ? log.events[log.events.length - 1].seq : 0;
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
data: {
|
|
265
|
+
events: page,
|
|
266
|
+
nextCursor: page.length ? page[page.length - 1].seq : Math.min(input.afterSequence, last),
|
|
267
|
+
lastEventSequence: last,
|
|
268
|
+
retainedFromSequence,
|
|
269
|
+
gap: input.afterSequence + 1 < retainedFromSequence,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return fail(error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-406 M0 — durable mission storage.
|
|
3
|
+
*
|
|
4
|
+
* Store decision (audit in the PR): ruflo's authoritative state already lives
|
|
5
|
+
* in project files guarded by a lock and replaced atomically (the policy
|
|
6
|
+
* ledger in `.claude-flow/policy/`, claims, workspace leases). `better-sqlite3`
|
|
7
|
+
* is an optional dependency the CLI must run without, and `sql.js` keeps the
|
|
8
|
+
* database in memory and persists by whole-file export, which is neither
|
|
9
|
+
* append-durable nor safe across processes. So a mission is:
|
|
10
|
+
*
|
|
11
|
+
* .claude-flow/missions/<missionId>/events.jsonl append-only, hash-chained
|
|
12
|
+
* .claude-flow/missions/<missionId>/snapshot.json materialized cache (rebuildable)
|
|
13
|
+
* .claude-flow/missions/<missionId>/lock per-mission writer lock
|
|
14
|
+
*
|
|
15
|
+
* The log is the truth; the snapshot is only a cache that must agree with it.
|
|
16
|
+
* Each event is one write of one line followed by fsync. A crash can leave at
|
|
17
|
+
* most an unterminated last line, which readers ignore and the next locked
|
|
18
|
+
* writer truncates. Writers take the policy runtime's lock (`acquireLock`),
|
|
19
|
+
* which recognises a dead owner by pid, pid namespace and boot id.
|
|
20
|
+
*/
|
|
21
|
+
import { type MissionEvent } from './schemas.js';
|
|
22
|
+
import type { MissionView } from './transitions.js';
|
|
23
|
+
export declare const MISSIONS_DIR: string;
|
|
24
|
+
export interface LogRead {
|
|
25
|
+
readonly events: MissionEvent[];
|
|
26
|
+
/** Bytes covered by complete lines; anything after is a torn tail. */
|
|
27
|
+
readonly validBytes: number;
|
|
28
|
+
readonly tornTail: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare class MissionStoreError extends Error {
|
|
31
|
+
readonly code: string;
|
|
32
|
+
constructor(code: string, message: string);
|
|
33
|
+
}
|
|
34
|
+
/** tmp + fsync + rename + directory fsync. */
|
|
35
|
+
export declare function writeFileAtomic(path: string, text: string, mode?: number): void;
|
|
36
|
+
export declare function parseLog(buffer: Buffer): LogRead;
|
|
37
|
+
export declare class MissionStore {
|
|
38
|
+
readonly root: string;
|
|
39
|
+
constructor(projectRoot: string);
|
|
40
|
+
missionDir(missionId: string): string;
|
|
41
|
+
private logPath;
|
|
42
|
+
private snapshotPath;
|
|
43
|
+
exists(missionId: string): boolean;
|
|
44
|
+
listMissionIds(): string[];
|
|
45
|
+
/** Atomic claim of a new mission id: `mkdir` fails if another request won. */
|
|
46
|
+
claim(missionId: string): boolean;
|
|
47
|
+
readLog(missionId: string): LogRead;
|
|
48
|
+
/** Snapshot when it agrees with the log, then fold the tail; otherwise full replay. */
|
|
49
|
+
loadView(missionId: string, log?: LogRead): MissionView | null;
|
|
50
|
+
/**
|
|
51
|
+
* Run `fn` under the mission's writer lock with a freshly replayed view.
|
|
52
|
+
* Events `fn` returns are appended (each fsynced) only after `fn` validated
|
|
53
|
+
* them, then the snapshot is refreshed.
|
|
54
|
+
*/
|
|
55
|
+
withMission<T>(missionId: string, fn: (view: MissionView | null, log: LogRead) => {
|
|
56
|
+
events: MissionEvent[];
|
|
57
|
+
result: T;
|
|
58
|
+
view: MissionView | null;
|
|
59
|
+
}): Promise<T>;
|
|
60
|
+
private append;
|
|
61
|
+
writeSnapshot(missionId: string, view: MissionView): void;
|
|
62
|
+
/** For diagnostics and tests: the log file's size in bytes. */
|
|
63
|
+
logSize(missionId: string): number;
|
|
64
|
+
/** Workspace-level lock for the observation file. */
|
|
65
|
+
withWorkspaceLock<T>(fn: () => T): Promise<T>;
|
|
66
|
+
writeWorkspaceFile(name: string, text: string): void;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=store.d.ts.map
|