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.
Files changed (66) hide show
  1. package/.claude/helpers/hook-handler.cjs +14 -1
  2. package/.claude/proven-config.json +1 -1
  3. package/.claude/settings.json +11 -2
  4. package/.claude-plugin/marketplace.json +15 -0
  5. package/package.json +1 -1
  6. package/v3/@claude-flow/cli/README.md +0 -2
  7. package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
  8. package/v3/@claude-flow/cli/dist/src/commands/catalog.d.ts +18 -0
  9. package/v3/@claude-flow/cli/dist/src/commands/catalog.js +89 -0
  10. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +24 -1
  11. package/v3/@claude-flow/cli/dist/src/commands/index.js +5 -0
  12. package/v3/@claude-flow/cli/dist/src/commands/init.js +60 -1
  13. package/v3/@claude-flow/cli/dist/src/commands/mission.d.ts +24 -0
  14. package/v3/@claude-flow/cli/dist/src/commands/mission.js +116 -0
  15. package/v3/@claude-flow/cli/dist/src/commands/mods.d.ts +13 -0
  16. package/v3/@claude-flow/cli/dist/src/commands/mods.js +170 -0
  17. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +12 -0
  18. package/v3/@claude-flow/cli/dist/src/mcp-client.js +3 -0
  19. package/v3/@claude-flow/cli/dist/src/mcp-tools/capability-brain.js +1 -1
  20. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +1 -0
  21. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +2 -0
  22. package/v3/@claude-flow/cli/dist/src/mcp-tools/mission-tools.d.ts +22 -0
  23. package/v3/@claude-flow/cli/dist/src/mcp-tools/mission-tools.js +75 -0
  24. package/v3/@claude-flow/cli/dist/src/missions/fold.d.ts +19 -0
  25. package/v3/@claude-flow/cli/dist/src/missions/fold.js +238 -0
  26. package/v3/@claude-flow/cli/dist/src/missions/index.d.ts +10 -0
  27. package/v3/@claude-flow/cli/dist/src/missions/index.js +10 -0
  28. package/v3/@claude-flow/cli/dist/src/missions/observation.d.ts +70 -0
  29. package/v3/@claude-flow/cli/dist/src/missions/observation.js +77 -0
  30. package/v3/@claude-flow/cli/dist/src/missions/schemas.d.ts +735 -0
  31. package/v3/@claude-flow/cli/dist/src/missions/schemas.js +150 -0
  32. package/v3/@claude-flow/cli/dist/src/missions/service.d.ts +88 -0
  33. package/v3/@claude-flow/cli/dist/src/missions/service.js +278 -0
  34. package/v3/@claude-flow/cli/dist/src/missions/store.d.ts +68 -0
  35. package/v3/@claude-flow/cli/dist/src/missions/store.js +209 -0
  36. package/v3/@claude-flow/cli/dist/src/missions/transitions.d.ts +81 -0
  37. package/v3/@claude-flow/cli/dist/src/missions/transitions.js +178 -0
  38. package/v3/@claude-flow/cli/dist/src/mods/apply.d.ts +50 -0
  39. package/v3/@claude-flow/cli/dist/src/mods/apply.js +186 -0
  40. package/v3/@claude-flow/cli/dist/src/mods/claude-installs.d.ts +29 -0
  41. package/v3/@claude-flow/cli/dist/src/mods/claude-installs.js +83 -0
  42. package/v3/@claude-flow/cli/dist/src/mods/command-registry/catalog.d.ts +45 -0
  43. package/v3/@claude-flow/cli/dist/src/mods/command-registry/catalog.js +234 -0
  44. package/v3/@claude-flow/cli/dist/src/mods/command-registry/engine-report.d.ts +66 -0
  45. package/v3/@claude-flow/cli/dist/src/mods/command-registry/engine-report.js +119 -0
  46. package/v3/@claude-flow/cli/dist/src/mods/command-registry/generate.d.ts +51 -0
  47. package/v3/@claude-flow/cli/dist/src/mods/command-registry/generate.js +166 -0
  48. package/v3/@claude-flow/cli/dist/src/mods/command-registry/index.d.ts +9 -0
  49. package/v3/@claude-flow/cli/dist/src/mods/command-registry/index.js +9 -0
  50. package/v3/@claude-flow/cli/dist/src/mods/command-registry/inventory.d.ts +63 -0
  51. package/v3/@claude-flow/cli/dist/src/mods/command-registry/inventory.js +230 -0
  52. package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +836 -0
  53. package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.js +137 -0
  54. package/v3/@claude-flow/cli/dist/src/mods/install.d.ts +102 -0
  55. package/v3/@claude-flow/cli/dist/src/mods/install.js +185 -0
  56. package/v3/@claude-flow/cli/dist/src/mods/plugin-repair.d.ts +81 -0
  57. package/v3/@claude-flow/cli/dist/src/mods/plugin-repair.js +120 -0
  58. package/v3/@claude-flow/cli/dist/src/mods/plugin-resolve.d.ts +63 -0
  59. package/v3/@claude-flow/cli/dist/src/mods/plugin-resolve.js +182 -0
  60. package/v3/@claude-flow/cli/dist/src/mods/policy-projection.d.ts +39 -0
  61. package/v3/@claude-flow/cli/dist/src/mods/policy-projection.js +65 -0
  62. package/v3/@claude-flow/cli/dist/src/mods/probe.d.ts +33 -0
  63. package/v3/@claude-flow/cli/dist/src/mods/probe.js +143 -0
  64. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +5 -0
  65. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +20 -1
  66. package/v3/@claude-flow/cli/package.json +1 -1
@@ -0,0 +1,209 @@
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 { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, truncateSync, writeSync, } from 'node:fs';
22
+ import { join, resolve } from 'node:path';
23
+ import { acquireLock } from '../services/policy-runtime.js';
24
+ import { MISSION_ID } from './schemas.js';
25
+ import { replay } from './fold.js';
26
+ export const MISSIONS_DIR = join('.claude-flow', 'missions');
27
+ export class MissionStoreError extends Error {
28
+ code;
29
+ constructor(code, message) {
30
+ super(`${code}: ${message}`);
31
+ this.code = code;
32
+ this.name = 'MissionStoreError';
33
+ }
34
+ }
35
+ function fsyncPath(path) {
36
+ let fd;
37
+ try {
38
+ fd = openSync(path, 'r');
39
+ fsyncSync(fd);
40
+ }
41
+ catch {
42
+ /* directory fsync is unsupported on some platforms; the file itself was synced */
43
+ }
44
+ finally {
45
+ if (fd !== undefined)
46
+ closeSync(fd);
47
+ }
48
+ }
49
+ /** tmp + fsync + rename + directory fsync. */
50
+ export function writeFileAtomic(path, text, mode = 0o600) {
51
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
52
+ const fd = openSync(tmp, 'w', mode);
53
+ try {
54
+ writeSync(fd, text);
55
+ fsyncSync(fd);
56
+ }
57
+ finally {
58
+ closeSync(fd);
59
+ }
60
+ renameSync(tmp, path);
61
+ fsyncPath(join(path, '..'));
62
+ }
63
+ export function parseLog(buffer) {
64
+ const events = [];
65
+ let offset = 0;
66
+ while (offset < buffer.length) {
67
+ const nl = buffer.indexOf(0x0a, offset);
68
+ if (nl === -1)
69
+ return { events, validBytes: offset, tornTail: true };
70
+ const line = buffer.subarray(offset, nl).toString('utf8');
71
+ try {
72
+ events.push(JSON.parse(line));
73
+ }
74
+ catch {
75
+ // A terminated line is a completed write; unparsable means tampering or corruption.
76
+ throw new MissionStoreError('mission-log-corrupt', `unparsable line at byte ${offset}`);
77
+ }
78
+ offset = nl + 1;
79
+ }
80
+ return { events, validBytes: offset, tornTail: false };
81
+ }
82
+ export class MissionStore {
83
+ root;
84
+ constructor(projectRoot) {
85
+ this.root = join(resolve(projectRoot), MISSIONS_DIR);
86
+ }
87
+ missionDir(missionId) {
88
+ if (!MISSION_ID.test(missionId))
89
+ throw new MissionStoreError('invalid-mission-id', missionId);
90
+ return join(this.root, missionId);
91
+ }
92
+ logPath(id) { return join(this.missionDir(id), 'events.jsonl'); }
93
+ snapshotPath(id) { return join(this.missionDir(id), 'snapshot.json'); }
94
+ exists(missionId) {
95
+ return existsSync(this.logPath(missionId));
96
+ }
97
+ listMissionIds() {
98
+ if (!existsSync(this.root))
99
+ return [];
100
+ return readdirSync(this.root).filter((n) => MISSION_ID.test(n) && existsSync(join(this.root, n, 'events.jsonl'))).sort();
101
+ }
102
+ /** Atomic claim of a new mission id: `mkdir` fails if another request won. */
103
+ claim(missionId) {
104
+ mkdirSync(this.root, { recursive: true, mode: 0o700 });
105
+ try {
106
+ mkdirSync(this.missionDir(missionId), { mode: 0o700 });
107
+ return true;
108
+ }
109
+ catch (error) {
110
+ if (error.code === 'EEXIST')
111
+ return false;
112
+ throw error;
113
+ }
114
+ }
115
+ readLog(missionId) {
116
+ const path = this.logPath(missionId);
117
+ if (!existsSync(path))
118
+ return { events: [], validBytes: 0, tornTail: false };
119
+ return parseLog(readFileSync(path));
120
+ }
121
+ /** Snapshot when it agrees with the log, then fold the tail; otherwise full replay. */
122
+ loadView(missionId, log = this.readLog(missionId)) {
123
+ const snapshotFile = this.snapshotPath(missionId);
124
+ if (existsSync(snapshotFile)) {
125
+ try {
126
+ const snap = JSON.parse(readFileSync(snapshotFile, 'utf8'));
127
+ const anchor = log.events[snap.sequence - 1];
128
+ if (anchor && anchor.hash === snap.hash && snap.view.lastHash === snap.hash) {
129
+ return replay(log.events.slice(snap.sequence), snap.view);
130
+ }
131
+ }
132
+ catch {
133
+ /* unreadable cache: rebuild from the log */
134
+ }
135
+ }
136
+ return replay(log.events);
137
+ }
138
+ /**
139
+ * Run `fn` under the mission's writer lock with a freshly replayed view.
140
+ * Events `fn` returns are appended (each fsynced) only after `fn` validated
141
+ * them, then the snapshot is refreshed.
142
+ */
143
+ async withMission(missionId, fn) {
144
+ const dir = this.missionDir(missionId);
145
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
146
+ const release = await acquireLock(join(dir, 'lock'));
147
+ try {
148
+ const log = this.readLog(missionId);
149
+ if (log.tornTail)
150
+ truncateSync(this.logPath(missionId), log.validBytes);
151
+ const view = this.loadView(missionId, log);
152
+ const outcome = fn(view, log);
153
+ if (outcome.events.length) {
154
+ this.append(missionId, outcome.events);
155
+ if (outcome.view)
156
+ this.writeSnapshot(missionId, outcome.view);
157
+ }
158
+ return outcome.result;
159
+ }
160
+ finally {
161
+ release();
162
+ }
163
+ }
164
+ append(missionId, events) {
165
+ const fd = openSync(this.logPath(missionId), 'a', 0o600);
166
+ try {
167
+ for (const event of events) {
168
+ const line = Buffer.from(`${JSON.stringify(event)}\n`, 'utf8');
169
+ let written = 0;
170
+ while (written < line.length)
171
+ written += writeSync(fd, line, written, line.length - written);
172
+ fsyncSync(fd);
173
+ }
174
+ }
175
+ finally {
176
+ closeSync(fd);
177
+ }
178
+ }
179
+ writeSnapshot(missionId, view) {
180
+ writeFileAtomic(this.snapshotPath(missionId), JSON.stringify({
181
+ schemaVersion: 1,
182
+ missionId,
183
+ sequence: view.record.lastEventSequence,
184
+ hash: view.lastHash,
185
+ view,
186
+ }));
187
+ }
188
+ /** For diagnostics and tests: the log file's size in bytes. */
189
+ logSize(missionId) {
190
+ const path = this.logPath(missionId);
191
+ return existsSync(path) ? statSync(path).size : 0;
192
+ }
193
+ /** Workspace-level lock for the observation file. */
194
+ async withWorkspaceLock(fn) {
195
+ mkdirSync(this.root, { recursive: true, mode: 0o700 });
196
+ const release = await acquireLock(join(this.root, 'observation.lock'));
197
+ try {
198
+ return fn();
199
+ }
200
+ finally {
201
+ release();
202
+ }
203
+ }
204
+ writeWorkspaceFile(name, text) {
205
+ mkdirSync(this.root, { recursive: true, mode: 0o700 });
206
+ writeFileAtomic(join(this.root, name), text, 0o644);
207
+ }
208
+ }
209
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * ADR-406 §19.4 — mission state transitions as a data table, and the reducer
3
+ * that folds an event log into a `MissionView`.
4
+ *
5
+ * The table is the single authority: `checkTransition` (before an append) and
6
+ * `applyEvent` (during replay) both consult it, so a log that replays is a log
7
+ * whose every control event was a permitted transition. Observations never
8
+ * change state or revision; a disconnected executor is an observation, not a
9
+ * failure.
10
+ */
11
+ import { type ControlEventType, type MissionRecord, type MissionState, type PlanBody } from './schemas.js';
12
+ export type TaskStatus = 'pending' | 'running' | 'recorded-done' | 'failed' | 'unknown';
13
+ export interface EvidenceEntry {
14
+ readonly evidenceId: string;
15
+ readonly criterionId: string;
16
+ readonly taskId: string | null;
17
+ readonly artifactDigest: string;
18
+ readonly producer: string;
19
+ /** True only when the service authenticated the producer; never caller-asserted. */
20
+ readonly verified: boolean;
21
+ readonly planRevision: number;
22
+ readonly criteriaDigest: string;
23
+ readonly at: string;
24
+ }
25
+ export interface BudgetLedger {
26
+ readonly currency: string;
27
+ readonly ceilingMinor: number;
28
+ readonly estimatedMinor: number;
29
+ readonly reservedMinor: number;
30
+ readonly settledMinor: number;
31
+ readonly unresolvedMinor: number;
32
+ }
33
+ export interface MissionView {
34
+ readonly record: MissionRecord;
35
+ readonly planBody: PlanBody | null;
36
+ readonly budget: BudgetLedger | null;
37
+ readonly tasks: Readonly<Record<string, {
38
+ readonly status: TaskStatus;
39
+ readonly invocationId?: string;
40
+ }>>;
41
+ readonly evidence: readonly EvidenceEntry[];
42
+ readonly executor: {
43
+ readonly executorId: string;
44
+ readonly connection: string;
45
+ readonly observedAt: string;
46
+ readonly lastAckExecutionId: string | null;
47
+ } | null;
48
+ readonly lease: {
49
+ readonly executorId: string | null;
50
+ readonly epoch: number;
51
+ };
52
+ readonly unresolvedOperations: readonly string[];
53
+ readonly blockedReason: string | null;
54
+ readonly lastHash: string;
55
+ }
56
+ export declare class TransitionError extends Error {
57
+ readonly code: string;
58
+ constructor(code: string, message: string);
59
+ }
60
+ type Guard = (view: MissionView, payload: Record<string, unknown>) => string | null;
61
+ export interface TransitionRule {
62
+ readonly event: ControlEventType;
63
+ readonly from: readonly MissionState[];
64
+ readonly to: MissionState;
65
+ readonly guard: Guard;
66
+ }
67
+ export declare function digestOf(value: unknown): string;
68
+ /** Well-formedness for §19.4 row 1: graph acyclic, deps resolvable, budget fits. */
69
+ export declare function validatePlan(raw: unknown): {
70
+ plan: PlanBody;
71
+ error: null;
72
+ } | {
73
+ plan: null;
74
+ error: string;
75
+ };
76
+ export declare const TRANSITIONS: readonly TransitionRule[];
77
+ export declare function ruleFor(event: ControlEventType, from: MissionState): TransitionRule | undefined;
78
+ /** Throws `TransitionError` unless `type` is permitted from the view's state. */
79
+ export declare function checkTransition(view: MissionView, type: ControlEventType, payload: Record<string, unknown>): TransitionRule;
80
+ export {};
81
+ //# sourceMappingURL=transitions.d.ts.map
@@ -0,0 +1,178 @@
1
+ /**
2
+ * ADR-406 §19.4 — mission state transitions as a data table, and the reducer
3
+ * that folds an event log into a `MissionView`.
4
+ *
5
+ * The table is the single authority: `checkTransition` (before an append) and
6
+ * `applyEvent` (during replay) both consult it, so a log that replays is a log
7
+ * whose every control event was a permitted transition. Observations never
8
+ * change state or revision; a disconnected executor is an observation, not a
9
+ * failure.
10
+ */
11
+ import { createHash } from 'node:crypto';
12
+ import { TERMINAL_STATES, planBodySchema, } from './schemas.js';
13
+ import { canonicalJson } from '../mods/command-registry/catalog.js';
14
+ export class TransitionError extends Error {
15
+ code;
16
+ constructor(code, message) {
17
+ super(`${code}: ${message}`);
18
+ this.code = code;
19
+ this.name = 'TransitionError';
20
+ }
21
+ }
22
+ const ACTIVE = ['queued', 'running', 'pauseRequested', 'paused', 'blocked', 'verifying'];
23
+ const NONTERMINAL = [
24
+ 'draft', 'planned', 'awaitingAuthorization', 'queued', 'running', 'pauseRequested', 'paused', 'blocked', 'verifying',
25
+ ];
26
+ // Loss of contact is never proof of failure (§19.3, §19.4 last row).
27
+ const CONNECTIVITY_CAUSES = new Set(['disconnected', 'timeout', 'connection-lost', 'unreachable', 'unknown', 'stale']);
28
+ export function digestOf(value) {
29
+ return `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
30
+ }
31
+ /** Well-formedness for §19.4 row 1: graph acyclic, deps resolvable, budget fits. */
32
+ export function validatePlan(raw) {
33
+ const parsed = planBodySchema.safeParse(raw);
34
+ if (!parsed.success)
35
+ return { plan: null, error: parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ') };
36
+ const plan = parsed.data;
37
+ const ids = new Set();
38
+ for (const t of plan.tasks) {
39
+ if (ids.has(t.id))
40
+ return { plan: null, error: `duplicate task id ${t.id}` };
41
+ ids.add(t.id);
42
+ }
43
+ const criteria = new Set(plan.acceptance.map((c) => c.id));
44
+ if (criteria.size !== plan.acceptance.length)
45
+ return { plan: null, error: 'duplicate acceptance criterion id' };
46
+ if (!plan.acceptance.some((c) => c.mandatory))
47
+ return { plan: null, error: 'at least one mandatory acceptance criterion is required' };
48
+ for (const t of plan.tasks) {
49
+ for (const d of t.dependsOn)
50
+ if (!ids.has(d))
51
+ return { plan: null, error: `task ${t.id} depends on unknown task ${d}` };
52
+ for (const c of t.acceptanceEvidence)
53
+ if (!criteria.has(c))
54
+ return { plan: null, error: `task ${t.id} names unknown criterion ${c}` };
55
+ }
56
+ // Kahn's algorithm: every task must be reachable from a dependency-free task.
57
+ const indegree = new Map(plan.tasks.map((t) => [t.id, t.dependsOn.length]));
58
+ const queue = plan.tasks.filter((t) => t.dependsOn.length === 0).map((t) => t.id);
59
+ let visited = 0;
60
+ while (queue.length) {
61
+ const id = queue.shift();
62
+ visited++;
63
+ for (const t of plan.tasks) {
64
+ if (!t.dependsOn.includes(id))
65
+ continue;
66
+ const left = (indegree.get(t.id) ?? 0) - 1;
67
+ indegree.set(t.id, left);
68
+ if (left === 0)
69
+ queue.push(t.id);
70
+ }
71
+ }
72
+ if (visited !== plan.tasks.length)
73
+ return { plan: null, error: 'task graph has a cycle' };
74
+ const estimated = plan.tasks.reduce((sum, t) => sum + t.estimatedCostMinor, 0);
75
+ if (estimated > plan.budget.ceilingMinor)
76
+ return { plan: null, error: `estimated cost ${estimated} exceeds ceiling ${plan.budget.ceilingMinor}` };
77
+ return { plan, error: null };
78
+ }
79
+ const planGuard = (mustDiffer) => (view, payload) => {
80
+ const result = validatePlan(payload.plan);
81
+ if (result.error !== null)
82
+ return `plan invalid: ${result.error}`;
83
+ if (payload.digest !== digestOf(result.plan))
84
+ return 'plan digest does not match plan body';
85
+ if (mustDiffer && payload.digest === view.record.plan.digest)
86
+ return 'plan unchanged';
87
+ return null;
88
+ };
89
+ const admissionGuard = (view, payload) => {
90
+ if (!view.planBody || !view.budget)
91
+ return 'no plan to admit';
92
+ if (payload.planDigest !== view.record.plan.digest)
93
+ return 'authorization does not cover the current plan revision';
94
+ if (typeof payload.authorizationRef !== 'string' || !payload.authorizationRef)
95
+ return 'authorizationRef required';
96
+ const reservation = payload.reservationMinor;
97
+ if (typeof reservation !== 'number' || !Number.isInteger(reservation) || reservation < 0)
98
+ return 'reservationMinor must be a non-negative integer';
99
+ const b = view.budget;
100
+ if (b.settledMinor + b.reservedMinor + b.unresolvedMinor + reservation > b.ceilingMinor)
101
+ return 'budget reservation exceeds the authorized ceiling';
102
+ return null;
103
+ };
104
+ const epochGuard = (view, payload) => payload.leaseEpoch === view.lease.epoch ? null : `stale lease epoch ${String(payload.leaseEpoch)} (current ${view.lease.epoch})`;
105
+ export const TRANSITIONS = Object.freeze([
106
+ { event: 'plan.validated', from: ['draft'], to: 'planned', guard: planGuard(false) },
107
+ { event: 'plan.revised', from: ['planned', 'awaitingAuthorization', 'paused', 'blocked'], to: 'planned', guard: planGuard(true) },
108
+ { event: 'authorization.missing', from: ['planned'], to: 'awaitingAuthorization', guard: (v) => (v.planBody ? null : 'no concrete plan') },
109
+ { event: 'admission.accepted', from: ['planned', 'awaitingAuthorization'], to: 'queued', guard: admissionGuard },
110
+ {
111
+ event: 'executor.acknowledged', from: ['queued'], to: 'running',
112
+ guard: (v, p) => (typeof p.executorId !== 'string' || !p.executorId ? 'executorId required' : epochGuard(v, p)),
113
+ },
114
+ { event: 'pause.accepted', from: ['running'], to: 'pauseRequested', guard: () => null },
115
+ { event: 'quiescence.confirmed', from: ['pauseRequested'], to: 'paused', guard: epochGuard },
116
+ { event: 'blocked', from: ['running', 'queued'], to: 'blocked', guard: (_v, p) => (typeof p.reason === 'string' && p.reason ? null : 'reason required') },
117
+ { event: 'resume.admitted', from: ['paused', 'blocked'], to: 'queued', guard: admissionGuard },
118
+ {
119
+ event: 'tasks.settled', from: ['running'], to: 'verifying',
120
+ guard: (v) => {
121
+ if (v.unresolvedOperations.length)
122
+ return `unresolved operations: ${v.unresolvedOperations.join(', ')}`;
123
+ const open = Object.entries(v.tasks).filter(([, t]) => t.status !== 'recorded-done' && t.status !== 'failed');
124
+ return open.length ? `tasks not settled: ${open.map(([id]) => id).join(', ')}` : null;
125
+ },
126
+ },
127
+ {
128
+ event: 'acceptance.passed', from: ['verifying'], to: 'completed',
129
+ guard: (v, p) => {
130
+ const artifact = p.artifactDigest;
131
+ if (typeof artifact !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(artifact))
132
+ return 'artifactDigest required';
133
+ for (const c of (v.planBody?.acceptance ?? []).filter((x) => x.mandatory)) {
134
+ const ok = v.evidence.some((e) => e.criterionId === c.id && e.verified
135
+ && e.planRevision === v.record.plan.revision
136
+ && e.criteriaDigest === v.record.acceptance.criteriaDigest
137
+ && e.artifactDigest === artifact);
138
+ if (!ok)
139
+ return `criterion ${c.id} lacks verified evidence for this plan, criteria and artifact`;
140
+ }
141
+ return null;
142
+ },
143
+ },
144
+ { event: 'acceptance.failed', from: ['verifying'], to: 'failed', guard: (_v, p) => (typeof p.reason === 'string' && p.reason ? null : 'reason required') },
145
+ { event: 'cancel.admitted', from: NONTERMINAL, to: 'cancelRequested', guard: () => null },
146
+ {
147
+ event: 'cancel.settled', from: ['cancelRequested'], to: 'cancelled',
148
+ guard: (v, p) => {
149
+ const acked = Array.isArray(p.acknowledgedExecutors) ? p.acknowledgedExecutors : [];
150
+ return v.lease.executorId && !acked.includes(v.lease.executorId) ? `executor ${v.lease.executorId} has not acknowledged settlement` : null;
151
+ },
152
+ },
153
+ {
154
+ event: 'failure.verified', from: ACTIVE, to: 'failed',
155
+ guard: (_v, p) => {
156
+ if (typeof p.cause !== 'string' || CONNECTIVITY_CAUSES.has(p.cause))
157
+ return 'lost connectivity is not verified failure';
158
+ return typeof p.evidenceRef === 'string' && p.evidenceRef ? null : 'evidenceRef required';
159
+ },
160
+ },
161
+ ]);
162
+ export function ruleFor(event, from) {
163
+ return TRANSITIONS.find((r) => r.event === event && r.from.includes(from));
164
+ }
165
+ /** Throws `TransitionError` unless `type` is permitted from the view's state. */
166
+ export function checkTransition(view, type, payload) {
167
+ const from = view.record.state;
168
+ if (TERMINAL_STATES.has(from))
169
+ throw new TransitionError('terminal', `mission is ${from}; its history is immutable`);
170
+ const rule = ruleFor(type, from);
171
+ if (!rule)
172
+ throw new TransitionError('invalid-transition', `${type} is not permitted from ${from}`);
173
+ const reason = rule.guard(view, payload);
174
+ if (reason)
175
+ throw new TransitionError('guard-failed', `${type} from ${from}: ${reason}`);
176
+ return rule;
177
+ }
178
+ //# sourceMappingURL=transitions.js.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Turn the ruflo mods on (or off) for a project (ADR-404): merge the
3
+ * settings, sync the policy projection, then make the plugins loadable with
4
+ * `claude`. One path for `ruflo mods install`, `ruflo init` (and its wizard)
5
+ * and `ruflo init upgrade --mods`; `removeMods` is `ruflo mods uninstall`.
6
+ */
7
+ import { type Added, type InstallResult, type MarketplaceEntry, type Scope } from './install.js';
8
+ import { type Exec } from './plugin-repair.js';
9
+ export interface ApplyOptions {
10
+ scope: Scope;
11
+ dryRun?: boolean;
12
+ /** Run `claude plugin marketplace …` and `claude plugin install …` (default true). */
13
+ pluginInstall?: boolean;
14
+ /**
15
+ * Skip the `claude` step under a test runner or CI (VITEST / CI set): an
16
+ * init there must never clone from GitHub into whatever config dir is live.
17
+ */
18
+ skipInTestEnv?: boolean;
19
+ /** The marketplace to declare when the settings have none (default github ruvnet/ruflo). */
20
+ marketplace?: MarketplaceEntry;
21
+ /** Injectable for tests. */
22
+ exec?: Exec;
23
+ }
24
+ export interface ApplyResult {
25
+ install: InstallResult;
26
+ /** Whether every required plugin was left loadable (true when the step was skipped by request). */
27
+ resolvable: boolean;
28
+ /** Why the `claude` step did not run, when it did not. */
29
+ skipped?: string;
30
+ }
31
+ /** One line per thing a run added; empty on a re-run. */
32
+ export declare function describeAdded(added: Added): string[];
33
+ /** What default-on means, said exactly (ADR-404 Amendment 1). */
34
+ export declare const LOAD_NOTE: string[];
35
+ export declare function applyMods(root: string, opts: ApplyOptions): Promise<ApplyResult>;
36
+ /**
37
+ * `ruflo mods uninstall`: `claude plugin uninstall` exactly the plugins ruflo
38
+ * installed, then remove exactly the settings keys it added.
39
+ */
40
+ export declare function removeMods(root: string, opts?: {
41
+ dryRun?: boolean;
42
+ exec?: Exec;
43
+ }): Promise<{
44
+ settingsFiles: string[];
45
+ removed: boolean;
46
+ dryRun: boolean;
47
+ uninstalled: string[];
48
+ failed: string[];
49
+ }>;
50
+ //# sourceMappingURL=apply.d.ts.map