claude-flow 3.50.0 → 3.51.1
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 +4 -3
- package/.claude/proven-config.json +1 -1
- package/.claude/settings.json +11 -2
- package/.claude-plugin/marketplace.json +5 -0
- package/README.md +3 -1
- 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/federation.js +2 -2
- 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/init/helpers-generator.js +4 -2
- 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/mcp-tools/seraphina-tools.js +15 -12
- package/v3/@claude-flow/cli/dist/src/mcp-tools/service-url.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/service-url.js +34 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/x-federation-tools.js +5 -4
- 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
|
@@ -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
|