@ours.network/fleet 1.1.0-nightly.8 → 1.1.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/README.md +231 -46
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +7 -4
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/role-creation-service.d.ts +4 -4
- package/dist/application/role-creation-service.js +9 -2
- package/dist/application/role-repository.d.ts +4 -2
- package/dist/application/role-repository.js +6 -3
- package/dist/application/session-control.d.ts +3 -1
- package/dist/application/session-control.js +4 -2
- package/dist/application/task-room-service.d.ts +92 -39
- package/dist/application/task-room-service.js +579 -173
- package/dist/briefing.js +85 -28
- package/dist/build-info.json +5 -5
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +130 -55
- package/dist/config.d.ts +38 -3
- package/dist/config.js +193 -28
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +193 -55
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +40 -4
- package/dist/fleet-command-audit.d.ts +59 -9
- package/dist/fleet-command-audit.js +426 -63
- package/dist/fleet-proxy.d.ts +5 -2
- package/dist/harness/claude-code-session.js +2 -1
- package/dist/harness/codex-app-server-proxy.d.ts +40 -0
- package/dist/harness/codex-app-server-proxy.js +348 -14
- package/dist/harness/codex-session.d.ts +9 -2
- package/dist/harness/codex-session.js +18 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +104 -10
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +1 -1
- package/dist/init-wizard.d.ts +98 -0
- package/dist/init-wizard.js +631 -0
- package/dist/lifecycle-summary.d.ts +134 -0
- package/dist/lifecycle-summary.js +179 -0
- package/dist/loops/config.d.ts +10 -1
- package/dist/loops/config.js +38 -5
- package/dist/model-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +21 -5
- package/dist/owner-channel/channel.js +312 -138
- package/dist/owner-channel/commands.d.ts +28 -6
- package/dist/owner-channel/commands.js +109 -27
- package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
- package/dist/owner-channel/lifecycle-outbox.js +87 -0
- package/dist/owner-channel/ours-client.d.ts +19 -0
- package/dist/owner-channel/ours-client.js +3 -0
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/permissions.js +1 -1
- package/dist/preset-migration.d.ts +42 -0
- package/dist/preset-migration.js +450 -0
- package/dist/reports/artifact.d.ts +10 -0
- package/dist/reports/artifact.js +32 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/render.d.ts +6 -0
- package/dist/reports/render.js +240 -0
- package/dist/reports/service.d.ts +42 -0
- package/dist/reports/service.js +57 -0
- package/dist/reports/tasks.d.ts +52 -0
- package/dist/reports/tasks.js +177 -0
- package/dist/reports/types.d.ts +315 -0
- package/dist/reports/types.js +6 -0
- package/dist/resolved-plan.js +13 -1
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +434 -279
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +23 -7
- package/dist/rooms-tasks/config.js +16 -25
- package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
- package/dist/rooms-tasks/cowork-adapter.js +13 -0
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/external-worker.d.ts +8 -0
- package/dist/rooms-tasks/external-worker.js +38 -0
- package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
- package/dist/rooms-tasks/launch-snapshot.js +181 -0
- package/dist/rooms-tasks/member-overrides.d.ts +30 -0
- package/dist/rooms-tasks/member-overrides.js +204 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +9 -2
- package/dist/rooms-tasks/provision.js +146 -56
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-room-name.d.ts +12 -0
- package/dist/rooms-tasks/task-room-name.js +61 -0
- package/dist/rooms-tasks/task-state.d.ts +115 -3
- package/dist/rooms-tasks/task-state.js +361 -11
- package/dist/rooms-tasks/templates.d.ts +4 -1
- package/dist/rooms-tasks/templates.js +34 -3
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +10 -4
- package/dist/rooms-tasks/types.d.ts +92 -7
- package/dist/rooms-tasks/types.js +14 -1
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +92 -33
- package/dist/session/acp.d.ts +4 -1
- package/dist/session/acp.js +29 -13
- package/dist/session/arbiter.d.ts +2 -1
- package/dist/session/arbiter.js +2 -0
- package/dist/session/codex-app-server-transport.d.ts +53 -0
- package/dist/session/codex-app-server-transport.js +184 -0
- package/dist/session/codex-app-server.d.ts +122 -0
- package/dist/session/codex-app-server.js +1031 -0
- package/dist/session/control.d.ts +2 -2
- package/dist/session/control.js +18 -2
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/types.d.ts +24 -6
- package/dist/session/types.js +28 -0
- package/dist/spawn.d.ts +9 -0
- package/dist/spawn.js +77 -9
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +25 -7
- package/dist/web/runtime.js +11 -6
- package/dist/web/server.js +28 -1
- package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
- package/dist/web-app/index.html +1 -1
- package/package.json +4 -4
- package/presets/brain-catalog.json +17 -0
- package/presets/fleet/{agents → agent_templates}/Critic.yaml +2 -0
- package/presets/fleet/{agents/Agent.yaml → agent_templates/Developer.yaml} +3 -1
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
- package/presets/fleet/agents/{Developer.yaml → FleetCoordinator.yaml} +1 -1
- package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +37 -0
- package/presets/fleet/roles/Critic.yaml +23 -1
- package/presets/fleet/roles/Developer.yaml +27 -5
- package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
- package/presets/fleet/room_templates/pair.yaml +9 -7
- package/presets/fleet/room_templates/single.yaml +5 -4
- package/presets/fleet/room_templates/team.yaml +12 -8
- package/presets/manifest.json +2 -1
- package/presets/fleet/agents/Architect.yaml +0 -3
- package/presets/fleet/agents/Secretary.yaml +0 -3
- package/presets/fleet/agents/Tester.yaml +0 -3
- package/presets/fleet/roles/Agent.yaml +0 -6
- package/presets/fleet/roles/Architect.yaml +0 -6
- package/presets/fleet/roles/Secretary.yaml +0 -6
- package/presets/fleet/roles/Tester.yaml +0 -6
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent } from './types.js';
|
|
1
|
+
import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent, TaskDeletionActor, TaskDeletionMemberPhase } from './types.js';
|
|
2
2
|
export declare const tasksDir: () => string;
|
|
3
3
|
export declare class TaskStateError extends Error {
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Accepted deletion is the per-task epoch guard: once pending, every lifecycle
|
|
7
|
+
* mutation, terminal settlement, and room publication must fail boundedly.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertNoPendingDeletion(task: TaskRecord): void;
|
|
5
10
|
export interface CreateTaskInput {
|
|
6
11
|
title: string;
|
|
7
12
|
brief?: string;
|
|
8
13
|
brief_file?: string;
|
|
9
14
|
template?: TaskTemplateRef;
|
|
15
|
+
execution_plan?: TaskRecord['execution_plan'];
|
|
10
16
|
origin: TaskOrigin;
|
|
11
17
|
idempotency_key?: string;
|
|
12
18
|
start?: boolean;
|
|
@@ -16,9 +22,11 @@ export interface CreateTaskInput {
|
|
|
16
22
|
}
|
|
17
23
|
export declare function createTask(input: CreateTaskInput): TaskRecord;
|
|
18
24
|
export declare function getTask(id: string): TaskRecord;
|
|
25
|
+
export declare function updateTaskExecutionPlan(id: string, executionPlan: NonNullable<TaskRecord['execution_plan']>): TaskRecord;
|
|
19
26
|
export declare function listTasks(filter?: {
|
|
20
27
|
state?: TaskState | TaskState[];
|
|
21
28
|
listId?: string;
|
|
29
|
+
includeDeleting?: boolean;
|
|
22
30
|
}): TaskRecord[];
|
|
23
31
|
export declare function moveTaskToList(id: string, listId: string): TaskRecord;
|
|
24
32
|
export declare function findByIdempotencyKey(key: string): TaskRecord | undefined;
|
|
@@ -43,5 +51,109 @@ export declare function failTask(id: string, error: string): TaskRecord;
|
|
|
43
51
|
export declare function updateTaskRoom(id: string, roomId: string, roomIdentityCid: string): TaskRecord;
|
|
44
52
|
export declare function updateTaskTemplate(id: string, template: TaskTemplateRef): TaskRecord;
|
|
45
53
|
export declare function updateTaskMembers(id: string, members: TaskMemberRole[]): TaskRecord;
|
|
46
|
-
/**
|
|
47
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Lenient read for deletion settlement and status surfaces: a broken list
|
|
56
|
+
* reference must never make a task unreadable or undeletable. Missing records
|
|
57
|
+
* throw the canonical task-not-found error; other read failures propagate.
|
|
58
|
+
*/
|
|
59
|
+
export declare function getDeletingTask(id: string): TaskRecord;
|
|
60
|
+
/** Cheap durable deletion-epoch probe for room/member publication guards. */
|
|
61
|
+
export declare function taskDeletionState(id: string): 'none' | 'pending' | 'absent';
|
|
62
|
+
/**
|
|
63
|
+
* Durable, surface-independent audit evidence for a permanent deletion. The
|
|
64
|
+
* receipt outlives the task record: written with the acceptance intent, and
|
|
65
|
+
* completed before settlement is reported. Metadata only — never brief or
|
|
66
|
+
* room content.
|
|
67
|
+
*/
|
|
68
|
+
export interface TaskDeletionReceipt {
|
|
69
|
+
schema_version: 1;
|
|
70
|
+
task_id: string;
|
|
71
|
+
title: string;
|
|
72
|
+
accepted_at: string;
|
|
73
|
+
actor: TaskDeletionActor;
|
|
74
|
+
original_state: TaskState;
|
|
75
|
+
room_id?: string;
|
|
76
|
+
member_count: number;
|
|
77
|
+
settled_at?: string;
|
|
78
|
+
result?: 'deleted';
|
|
79
|
+
}
|
|
80
|
+
export declare const deletionReceiptsDir: () => string;
|
|
81
|
+
export declare function readTaskDeletionReceipt(id: string): TaskDeletionReceipt | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Ensure the acceptance receipt exists before any cleanup side effect,
|
|
84
|
+
* backfilling it from the durable intent (heals a crash between the intent
|
|
85
|
+
* write and the receipt write). Fails closed: a receipt write error aborts
|
|
86
|
+
* settlement rather than deleting resources without audit evidence.
|
|
87
|
+
*/
|
|
88
|
+
export declare function ensureTaskDeletionReceipt(id: string): void;
|
|
89
|
+
/** Record settlement on the receipt; idempotent, tolerant of legacy absence. */
|
|
90
|
+
export declare function completeTaskDeletionReceipt(id: string): void;
|
|
91
|
+
export type TaskDeletionAcceptance = {
|
|
92
|
+
status: 'accepted' | 'pending';
|
|
93
|
+
task: TaskRecord;
|
|
94
|
+
} | {
|
|
95
|
+
status: 'already_absent';
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Persist the first-wins durable deletion intent. Accepts every lifecycle
|
|
99
|
+
* state, takes precedence over a pending terminal intent, and never touches
|
|
100
|
+
* remote resources. Repeat requests while pending re-arm settlement; a request
|
|
101
|
+
* for a missing record reports the idempotent already-absent outcome.
|
|
102
|
+
*
|
|
103
|
+
* Low-level record mutation only. Callers serialize acceptance with settlement
|
|
104
|
+
* through deletion.ts, which holds the common task-operation lock.
|
|
105
|
+
*/
|
|
106
|
+
export declare function beginTaskDeletionIntent(id: string, actor: TaskDeletionActor): TaskDeletionAcceptance;
|
|
107
|
+
/** Record an actionable settlement failure; the task stays hidden and recoverable. */
|
|
108
|
+
export declare function setTaskDeletionError(id: string, error: string, recoveryHint: string): TaskRecord;
|
|
109
|
+
/** Marker proving a member never launched, mirroring close.ts's short path. */
|
|
110
|
+
export declare const DELETION_MEMBER_NEVER_LAUNCHED = "never-launched";
|
|
111
|
+
/** Marker proving absence verified against temp state AND the CID-wide identity scan. */
|
|
112
|
+
export declare const DELETION_MEMBER_ABSENT_VERIFIED = "absent-verified";
|
|
113
|
+
/**
|
|
114
|
+
* Advance a member retirement cursor carried by the deletion intent. Only
|
|
115
|
+
* adjacent forward transitions are legal — plus the explicit
|
|
116
|
+
* pending → identity_absent short path proved by the 'never-launched' marker —
|
|
117
|
+
* so no managed agent can be claimed retired without the full evidence chain.
|
|
118
|
+
* launch_id and archive_path are immutable once recorded; same-phase retries
|
|
119
|
+
* are idempotent but must not change evidence.
|
|
120
|
+
*/
|
|
121
|
+
export declare function advanceTaskDeletionMember(id: string, name: string, phase: TaskDeletionMemberPhase, launchId?: string, archivePath?: string): TaskRecord;
|
|
122
|
+
interface SeatEvidence {
|
|
123
|
+
role_name: string;
|
|
124
|
+
identity_cid?: string;
|
|
125
|
+
retirement?: {
|
|
126
|
+
phase: TaskDeletionMemberPhase;
|
|
127
|
+
launch_id: string;
|
|
128
|
+
archive_path?: string;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Durably register cursors for late-provisioned members before retirement
|
|
133
|
+
* begins: provisioning publishes room seats before updateTaskMembers, so the
|
|
134
|
+
* acceptance snapshot can be empty while live seats exist. Seats without a
|
|
135
|
+
* recorded identity CID are not registered here — until a CID is recorded
|
|
136
|
+
* there is no managed identity to orphan, and the room record still carries
|
|
137
|
+
* those seats through the close saga that follows.
|
|
138
|
+
*/
|
|
139
|
+
export declare function upsertTaskDeletionMembersFromSeats(id: string, seats: ReadonlyArray<SeatEvidence>): TaskRecord;
|
|
140
|
+
/**
|
|
141
|
+
* The pre-room-delete checkpoint: import completed retirement evidence from
|
|
142
|
+
* room seats into the deletion cursors, so a crash between room-record
|
|
143
|
+
* deletion and task unlink retries from durable cursors instead of
|
|
144
|
+
* reconstructing consumed evidence. Call ONLY after closeManagedRoom has
|
|
145
|
+
* completed retirement and BEFORE cowork.deleteRoom/deleteRoomRecord.
|
|
146
|
+
*
|
|
147
|
+
* The room saga is trusted for phase jumps, but partial or corrupt retained
|
|
148
|
+
* records must not become false success: every seat must be identity_absent;
|
|
149
|
+
* a real launch requires archive evidence; an identity-less seat is accepted
|
|
150
|
+
* only via the never-launched proof.
|
|
151
|
+
*/
|
|
152
|
+
export declare function importTaskDeletionRetirementEvidence(id: string, seats: ReadonlyArray<SeatEvidence>): TaskRecord;
|
|
153
|
+
/**
|
|
154
|
+
* Physically remove a deletion-pending task record. Settlement-only: callers
|
|
155
|
+
* must have completed member retirement and room cleanup first. Missing
|
|
156
|
+
* records are the idempotent already-settled outcome.
|
|
157
|
+
*/
|
|
158
|
+
export declare function unlinkDeletedTask(id: string): boolean;
|
|
159
|
+
export {};
|
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
5
5
|
import { stateRoot } from '../paths.js';
|
|
6
|
-
import { TASK_TERMINAL_STATES, TASK_CANCELLABLE_STATES } from './types.js';
|
|
6
|
+
import { storedRoomLaunchPolicy, TASK_TERMINAL_STATES, TASK_CANCELLABLE_STATES } from './types.js';
|
|
7
7
|
import { DEFAULT_TASK_LIST_ID, readTaskLists } from './task-lists.js';
|
|
8
8
|
export const tasksDir = () => join(stateRoot(), 'tasks');
|
|
9
9
|
function taskPath(id) { return join(tasksDir(), `${id}.json`); }
|
|
@@ -116,11 +116,27 @@ function assertNoPendingTerminalIntent(task) {
|
|
|
116
116
|
throw new TaskStateError(`task ${task.task_id} has a pending '${task.terminal_intent.kind}' terminal intent`);
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Accepted deletion is the per-task epoch guard: once pending, every lifecycle
|
|
121
|
+
* mutation, terminal settlement, and room publication must fail boundedly.
|
|
122
|
+
*/
|
|
123
|
+
export function assertNoPendingDeletion(task) {
|
|
124
|
+
if (task.deletion?.status === 'pending') {
|
|
125
|
+
throw new TaskStateError(`task ${task.task_id} is pending deletion; run 'ours-fleet task delete ${task.task_id} ${task.task_id}' to retry cleanup`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
119
128
|
export function createTask(input) {
|
|
120
129
|
const key = input.idempotency_key ?? randomUUID();
|
|
121
130
|
const existing = findByIdempotencyKey(key);
|
|
122
|
-
if (existing)
|
|
131
|
+
if (existing) {
|
|
132
|
+
const existingPlan = existing.execution_plan?.plan_hash;
|
|
133
|
+
const requestedPlan = input.execution_plan?.plan_hash;
|
|
134
|
+
const existingPolicy = storedRoomLaunchPolicy(existing.execution_plan?.room_policy);
|
|
135
|
+
const requestedPolicy = storedRoomLaunchPolicy(input.execution_plan?.room_policy);
|
|
136
|
+
if (existingPlan !== requestedPlan || JSON.stringify(existingPolicy) !== JSON.stringify(requestedPolicy))
|
|
137
|
+
throw new TaskStateError(`idempotency key '${key}' was already used with a different execution plan`);
|
|
123
138
|
return existing;
|
|
139
|
+
}
|
|
124
140
|
const state = input.start === false ? 'backlog' : 'provisioning';
|
|
125
141
|
const record = {
|
|
126
142
|
task_id: generateTaskId(),
|
|
@@ -130,6 +146,7 @@ export function createTask(input) {
|
|
|
130
146
|
brief_file: input.brief_file,
|
|
131
147
|
state,
|
|
132
148
|
template: input.template,
|
|
149
|
+
execution_plan: input.execution_plan,
|
|
133
150
|
no_room: input.no_room || undefined,
|
|
134
151
|
room_id: input.room_id,
|
|
135
152
|
member_roles: [],
|
|
@@ -142,6 +159,25 @@ export function createTask(input) {
|
|
|
142
159
|
return readTask(record.task_id);
|
|
143
160
|
}
|
|
144
161
|
export function getTask(id) { return withTaskLock(id, () => readTask(id)); }
|
|
162
|
+
export function updateTaskExecutionPlan(id, executionPlan) {
|
|
163
|
+
return withTaskLock(id, () => {
|
|
164
|
+
const task = readTask(id);
|
|
165
|
+
assertNoPendingDeletion(task);
|
|
166
|
+
if (task.execution_plan) {
|
|
167
|
+
if (task.execution_plan.plan_hash !== executionPlan.plan_hash)
|
|
168
|
+
throw new TaskStateError(`task ${id} execution plan mismatch`);
|
|
169
|
+
const before = storedRoomLaunchPolicy(task.execution_plan.room_policy);
|
|
170
|
+
const after = storedRoomLaunchPolicy(executionPlan.room_policy);
|
|
171
|
+
if (task.room_id && JSON.stringify(before) !== JSON.stringify(after))
|
|
172
|
+
throw new TaskStateError(`task ${id} Room launch policy cannot change after Room creation`);
|
|
173
|
+
}
|
|
174
|
+
task.execution_plan = executionPlan;
|
|
175
|
+
task.template = { name: executionPlan.snapshot.name, version: executionPlan.snapshot.version,
|
|
176
|
+
content_hash: executionPlan.snapshot.content_hash };
|
|
177
|
+
writeTask(task);
|
|
178
|
+
return readTask(id);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
145
181
|
export function listTasks(filter) {
|
|
146
182
|
const dir = tasksDir();
|
|
147
183
|
if (!existsSync(dir))
|
|
@@ -154,6 +190,8 @@ export function listTasks(filter) {
|
|
|
154
190
|
try {
|
|
155
191
|
const id = f.slice(0, -5);
|
|
156
192
|
const t = withTaskLock(id, () => presentTask(JSON.parse(readFileSync(join(dir, f), 'utf8'))));
|
|
193
|
+
if (t.deletion?.status === 'pending' && !filter?.includeDeleting)
|
|
194
|
+
continue;
|
|
157
195
|
if ((!states || states.includes(t.state)) && (!filter?.listId || t.list_id === filter.listId))
|
|
158
196
|
tasks.push(t);
|
|
159
197
|
}
|
|
@@ -166,6 +204,7 @@ export function listTasks(filter) {
|
|
|
166
204
|
export function moveTaskToList(id, listId) {
|
|
167
205
|
return withTaskLock(id, () => {
|
|
168
206
|
const task = readTask(id);
|
|
207
|
+
assertNoPendingDeletion(task);
|
|
169
208
|
task.list_id = listId;
|
|
170
209
|
writeTask(task);
|
|
171
210
|
return readTask(id);
|
|
@@ -177,6 +216,7 @@ export function findByIdempotencyKey(key) {
|
|
|
177
216
|
export function startTask(id) {
|
|
178
217
|
return withTaskLock(id, () => {
|
|
179
218
|
const t = readTask(id);
|
|
219
|
+
assertNoPendingDeletion(t);
|
|
180
220
|
assertNoPendingTerminalIntent(t);
|
|
181
221
|
assertTransition(t.state, 'provisioning');
|
|
182
222
|
t.state = 'provisioning';
|
|
@@ -188,6 +228,7 @@ export function startTask(id) {
|
|
|
188
228
|
export function activateTask(id) {
|
|
189
229
|
return withTaskLock(id, () => {
|
|
190
230
|
const t = readTask(id);
|
|
231
|
+
assertNoPendingDeletion(t);
|
|
191
232
|
assertNoPendingTerminalIntent(t);
|
|
192
233
|
assertTransition(t.state, 'active');
|
|
193
234
|
t.state = 'active';
|
|
@@ -199,6 +240,7 @@ export function activateTask(id) {
|
|
|
199
240
|
export function blockTask(id, reason) {
|
|
200
241
|
return withTaskLock(id, () => {
|
|
201
242
|
const t = readTask(id);
|
|
243
|
+
assertNoPendingDeletion(t);
|
|
202
244
|
assertNoPendingTerminalIntent(t);
|
|
203
245
|
if (TASK_TERMINAL_STATES.includes(t.state))
|
|
204
246
|
throw new TaskStateError(`cannot block a '${t.state}' task`);
|
|
@@ -210,6 +252,7 @@ export function blockTask(id, reason) {
|
|
|
210
252
|
export function unblockTask(id) {
|
|
211
253
|
return withTaskLock(id, () => {
|
|
212
254
|
const t = readTask(id);
|
|
255
|
+
assertNoPendingDeletion(t);
|
|
213
256
|
assertNoPendingTerminalIntent(t);
|
|
214
257
|
if (!t.blocked)
|
|
215
258
|
throw new TaskStateError('task is not blocked');
|
|
@@ -221,6 +264,7 @@ export function unblockTask(id) {
|
|
|
221
264
|
export function reviewTask(id) {
|
|
222
265
|
return withTaskLock(id, () => {
|
|
223
266
|
const t = readTask(id);
|
|
267
|
+
assertNoPendingDeletion(t);
|
|
224
268
|
assertNoPendingTerminalIntent(t);
|
|
225
269
|
assertTransition(t.state, 'review');
|
|
226
270
|
t.state = 'review';
|
|
@@ -232,6 +276,7 @@ export function reviewTask(id) {
|
|
|
232
276
|
export function completeTask(id, outcome) {
|
|
233
277
|
return withTaskLock(id, () => {
|
|
234
278
|
const t = readTask(id);
|
|
279
|
+
assertNoPendingDeletion(t);
|
|
235
280
|
assertNoPendingTerminalIntent(t);
|
|
236
281
|
assertTransition(t.state, 'done');
|
|
237
282
|
t.state = 'done';
|
|
@@ -246,6 +291,7 @@ export function completeTask(id, outcome) {
|
|
|
246
291
|
export function cancelTask(id) {
|
|
247
292
|
return withTaskLock(id, () => {
|
|
248
293
|
const t = readTask(id);
|
|
294
|
+
assertNoPendingDeletion(t);
|
|
249
295
|
assertNoPendingTerminalIntent(t);
|
|
250
296
|
if (!TASK_CANCELLABLE_STATES.includes(t.state))
|
|
251
297
|
throw new TaskStateError(`cannot cancel a '${t.state}' task`);
|
|
@@ -263,6 +309,7 @@ function sameOutcome(left, right) {
|
|
|
263
309
|
export function beginTaskTerminalIntent(id, input) {
|
|
264
310
|
return withTaskLock(id, () => {
|
|
265
311
|
const t = readTask(id);
|
|
312
|
+
assertNoPendingDeletion(t);
|
|
266
313
|
const existing = t.terminal_intent;
|
|
267
314
|
if (existing) {
|
|
268
315
|
if (existing.kind !== input.kind || existing.room_id !== input.roomId
|
|
@@ -316,6 +363,7 @@ export function setTaskTerminalIntentError(id, error, recoveryHint) {
|
|
|
316
363
|
export function finishTaskTerminalIntent(id) {
|
|
317
364
|
return withTaskLock(id, () => {
|
|
318
365
|
const t = readTask(id);
|
|
366
|
+
assertNoPendingDeletion(t);
|
|
319
367
|
const intent = t.terminal_intent;
|
|
320
368
|
if (!intent)
|
|
321
369
|
throw new TaskStateError(`task ${id} has no terminal intent`);
|
|
@@ -346,6 +394,7 @@ export function finishTaskTerminalIntent(id) {
|
|
|
346
394
|
export function failTask(id, error) {
|
|
347
395
|
return withTaskLock(id, () => {
|
|
348
396
|
const t = readTask(id);
|
|
397
|
+
assertNoPendingDeletion(t);
|
|
349
398
|
assertNoPendingTerminalIntent(t);
|
|
350
399
|
assertTransition(t.state, 'failed');
|
|
351
400
|
t.state = 'failed';
|
|
@@ -359,6 +408,7 @@ export function failTask(id, error) {
|
|
|
359
408
|
export function updateTaskRoom(id, roomId, roomIdentityCid) {
|
|
360
409
|
return withTaskLock(id, () => {
|
|
361
410
|
const t = readTask(id);
|
|
411
|
+
assertNoPendingDeletion(t);
|
|
362
412
|
assertNoPendingTerminalIntent(t);
|
|
363
413
|
t.room_id = roomId;
|
|
364
414
|
t.room_identity_cid = roomIdentityCid;
|
|
@@ -369,6 +419,7 @@ export function updateTaskRoom(id, roomId, roomIdentityCid) {
|
|
|
369
419
|
export function updateTaskTemplate(id, template) {
|
|
370
420
|
return withTaskLock(id, () => {
|
|
371
421
|
const t = readTask(id);
|
|
422
|
+
assertNoPendingDeletion(t);
|
|
372
423
|
assertNoPendingTerminalIntent(t);
|
|
373
424
|
t.template = template;
|
|
374
425
|
writeTask(t);
|
|
@@ -378,31 +429,330 @@ export function updateTaskTemplate(id, template) {
|
|
|
378
429
|
export function updateTaskMembers(id, members) {
|
|
379
430
|
return withTaskLock(id, () => {
|
|
380
431
|
const t = readTask(id);
|
|
432
|
+
assertNoPendingDeletion(t);
|
|
381
433
|
assertNoPendingTerminalIntent(t);
|
|
382
434
|
t.member_roles = members;
|
|
383
435
|
writeTask(t);
|
|
384
436
|
return t;
|
|
385
437
|
});
|
|
386
438
|
}
|
|
387
|
-
|
|
388
|
-
|
|
439
|
+
function presentTaskLenient(record) {
|
|
440
|
+
try {
|
|
441
|
+
return presentTask(record);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// A broken list reference must never make a task undeletable.
|
|
445
|
+
const listId = record.list_id ?? DEFAULT_TASK_LIST_ID;
|
|
446
|
+
return { ...record, list_id: listId, list_name: listId === DEFAULT_TASK_LIST_ID ? 'default' : listId };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Lenient read for deletion settlement and status surfaces: a broken list
|
|
451
|
+
* reference must never make a task unreadable or undeletable. Missing records
|
|
452
|
+
* throw the canonical task-not-found error; other read failures propagate.
|
|
453
|
+
*/
|
|
454
|
+
export function getDeletingTask(id) {
|
|
389
455
|
return withTaskLock(id, () => {
|
|
390
456
|
assertCanonicalTaskId(id);
|
|
391
|
-
const p = taskPath(id);
|
|
392
|
-
let task;
|
|
393
457
|
try {
|
|
394
|
-
|
|
458
|
+
return presentTaskLenient(JSON.parse(readFileSync(taskPath(id), 'utf8')));
|
|
395
459
|
}
|
|
396
460
|
catch (error) {
|
|
397
461
|
if (isNotFoundError(error))
|
|
398
|
-
|
|
462
|
+
throw new TaskStateError(`task not found: ${id}`);
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
/** Cheap durable deletion-epoch probe for room/member publication guards. */
|
|
468
|
+
export function taskDeletionState(id) {
|
|
469
|
+
return withTaskLock(id, () => {
|
|
470
|
+
assertCanonicalTaskId(id);
|
|
471
|
+
try {
|
|
472
|
+
const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
|
|
473
|
+
return stored.deletion?.status === 'pending' ? 'pending' : 'none';
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
if (isNotFoundError(error))
|
|
477
|
+
return 'absent';
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
export const deletionReceiptsDir = () => join(stateRoot(), 'deletion-receipts');
|
|
483
|
+
function deletionReceiptPath(id) {
|
|
484
|
+
return join(deletionReceiptsDir(), `${id}.json`);
|
|
485
|
+
}
|
|
486
|
+
export function readTaskDeletionReceipt(id) {
|
|
487
|
+
assertCanonicalTaskId(id);
|
|
488
|
+
try {
|
|
489
|
+
return JSON.parse(readFileSync(deletionReceiptPath(id), 'utf8'));
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
if (isNotFoundError(error))
|
|
493
|
+
return undefined;
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function writeDeletionReceiptForIntent(stored) {
|
|
498
|
+
const deletion = stored.deletion;
|
|
499
|
+
mkdirSync(deletionReceiptsDir(), { recursive: true });
|
|
500
|
+
const receipt = {
|
|
501
|
+
schema_version: 1,
|
|
502
|
+
task_id: stored.task_id,
|
|
503
|
+
title: stored.title.slice(0, 256),
|
|
504
|
+
accepted_at: deletion.accepted_at,
|
|
505
|
+
actor: deletion.actor,
|
|
506
|
+
original_state: stored.state,
|
|
507
|
+
room_id: deletion.room_id,
|
|
508
|
+
member_count: deletion.members.length,
|
|
509
|
+
};
|
|
510
|
+
replaceFileAtomically(deletionReceiptPath(stored.task_id), JSON.stringify(receipt, null, 2) + '\n');
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Ensure the acceptance receipt exists before any cleanup side effect,
|
|
514
|
+
* backfilling it from the durable intent (heals a crash between the intent
|
|
515
|
+
* write and the receipt write). Fails closed: a receipt write error aborts
|
|
516
|
+
* settlement rather than deleting resources without audit evidence.
|
|
517
|
+
*/
|
|
518
|
+
export function ensureTaskDeletionReceipt(id) {
|
|
519
|
+
return withTaskLock(id, () => {
|
|
520
|
+
assertCanonicalTaskId(id);
|
|
521
|
+
const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
|
|
522
|
+
if (stored.deletion?.status !== 'pending')
|
|
523
|
+
throw new TaskStateError(`task ${id} has no pending deletion`);
|
|
524
|
+
if (!readTaskDeletionReceipt(id))
|
|
525
|
+
writeDeletionReceiptForIntent(stored);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
/** Record settlement on the receipt; idempotent, tolerant of legacy absence. */
|
|
529
|
+
export function completeTaskDeletionReceipt(id) {
|
|
530
|
+
const receipt = readTaskDeletionReceipt(id);
|
|
531
|
+
if (!receipt || receipt.settled_at)
|
|
532
|
+
return;
|
|
533
|
+
receipt.settled_at = new Date().toISOString();
|
|
534
|
+
receipt.result = 'deleted';
|
|
535
|
+
replaceFileAtomically(deletionReceiptPath(id), JSON.stringify(receipt, null, 2) + '\n');
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Persist the first-wins durable deletion intent. Accepts every lifecycle
|
|
539
|
+
* state, takes precedence over a pending terminal intent, and never touches
|
|
540
|
+
* remote resources. Repeat requests while pending re-arm settlement; a request
|
|
541
|
+
* for a missing record reports the idempotent already-absent outcome.
|
|
542
|
+
*
|
|
543
|
+
* Low-level record mutation only. Callers serialize acceptance with settlement
|
|
544
|
+
* through deletion.ts, which holds the common task-operation lock.
|
|
545
|
+
*/
|
|
546
|
+
export function beginTaskDeletionIntent(id, actor) {
|
|
547
|
+
return withTaskLock(id, () => {
|
|
548
|
+
assertCanonicalTaskId(id);
|
|
549
|
+
let stored;
|
|
550
|
+
try {
|
|
551
|
+
stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
if (isNotFoundError(error))
|
|
555
|
+
return { status: 'already_absent' };
|
|
399
556
|
throw error;
|
|
400
557
|
}
|
|
401
|
-
if (
|
|
402
|
-
|
|
558
|
+
if (stored.deletion?.status === 'pending') {
|
|
559
|
+
// Heal a crash between the intent write and the receipt write.
|
|
560
|
+
if (!readTaskDeletionReceipt(id))
|
|
561
|
+
writeDeletionReceiptForIntent(stored);
|
|
562
|
+
return { status: 'pending', task: presentTaskLenient(stored) };
|
|
563
|
+
}
|
|
564
|
+
const now = new Date().toISOString();
|
|
565
|
+
stored.deletion = {
|
|
566
|
+
status: 'pending',
|
|
567
|
+
accepted_at: now,
|
|
568
|
+
actor,
|
|
569
|
+
room_id: stored.room_id,
|
|
570
|
+
members: (stored.member_roles ?? []).map(member => ({
|
|
571
|
+
name: member.name, identity_cid: member.identity_cid, phase: 'pending', updated_at: now,
|
|
572
|
+
})),
|
|
573
|
+
};
|
|
574
|
+
writeTask(stored);
|
|
575
|
+
writeDeletionReceiptForIntent(stored);
|
|
576
|
+
return { status: 'accepted', task: presentTaskLenient(stored) };
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/** Record an actionable settlement failure; the task stays hidden and recoverable. */
|
|
580
|
+
export function setTaskDeletionError(id, error, recoveryHint) {
|
|
581
|
+
return withTaskLock(id, () => {
|
|
582
|
+
assertCanonicalTaskId(id);
|
|
583
|
+
const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
|
|
584
|
+
if (stored.deletion?.status !== 'pending')
|
|
585
|
+
throw new TaskStateError(`task ${id} has no pending deletion`);
|
|
586
|
+
stored.deletion.first_failure ??= error;
|
|
587
|
+
stored.deletion.first_recovery_hint ??= recoveryHint;
|
|
588
|
+
stored.deletion.error = error;
|
|
589
|
+
const previousErrorAt = Date.parse(stored.deletion.error_at ?? '');
|
|
590
|
+
stored.deletion.error_at = new Date(Math.max(Date.now(), Number.isFinite(previousErrorAt) ? previousErrorAt + 1 : 0)).toISOString();
|
|
591
|
+
stored.deletion.recovery_hint = recoveryHint;
|
|
592
|
+
writeTask(stored);
|
|
593
|
+
return presentTaskLenient(stored);
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
const DELETION_MEMBER_PHASE_ORDER = {
|
|
597
|
+
pending: 0, stop_requested: 1, liveness_absent: 2, archive_secured: 3, identity_absent: 4,
|
|
598
|
+
};
|
|
599
|
+
/** Marker proving a member never launched, mirroring close.ts's short path. */
|
|
600
|
+
export const DELETION_MEMBER_NEVER_LAUNCHED = 'never-launched';
|
|
601
|
+
/** Marker proving absence verified against temp state AND the CID-wide identity scan. */
|
|
602
|
+
export const DELETION_MEMBER_ABSENT_VERIFIED = 'absent-verified';
|
|
603
|
+
/**
|
|
604
|
+
* Advance a member retirement cursor carried by the deletion intent. Only
|
|
605
|
+
* adjacent forward transitions are legal — plus the explicit
|
|
606
|
+
* pending → identity_absent short path proved by the 'never-launched' marker —
|
|
607
|
+
* so no managed agent can be claimed retired without the full evidence chain.
|
|
608
|
+
* launch_id and archive_path are immutable once recorded; same-phase retries
|
|
609
|
+
* are idempotent but must not change evidence.
|
|
610
|
+
*/
|
|
611
|
+
export function advanceTaskDeletionMember(id, name, phase, launchId, archivePath) {
|
|
612
|
+
return withTaskLock(id, () => {
|
|
613
|
+
assertCanonicalTaskId(id);
|
|
614
|
+
const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
|
|
615
|
+
if (stored.deletion?.status !== 'pending')
|
|
616
|
+
throw new TaskStateError(`task ${id} has no pending deletion`);
|
|
617
|
+
const cursor = stored.deletion.members.find(member => member.name === name);
|
|
618
|
+
if (!cursor)
|
|
619
|
+
throw new TaskStateError(`task ${id} deletion has no member cursor '${name}'`);
|
|
620
|
+
if (cursor.launch_id !== undefined && launchId !== undefined && launchId !== cursor.launch_id)
|
|
621
|
+
throw new TaskStateError(`task ${id} deletion member '${name}' launch ownership proof is immutable`);
|
|
622
|
+
if (cursor.archive_path !== undefined && archivePath !== undefined && archivePath !== cursor.archive_path)
|
|
623
|
+
throw new TaskStateError(`task ${id} deletion member '${name}' archive evidence is immutable`);
|
|
624
|
+
const step = DELETION_MEMBER_PHASE_ORDER[phase] - DELETION_MEMBER_PHASE_ORDER[cursor.phase];
|
|
625
|
+
const launch = cursor.launch_id ?? launchId;
|
|
626
|
+
if (step < 0)
|
|
627
|
+
throw new TaskStateError(`task ${id} deletion member '${name}' cannot move back from '${cursor.phase}' to '${phase}'`);
|
|
628
|
+
if (step === 0)
|
|
629
|
+
return presentTaskLenient(stored); // idempotent retry, evidence already verified
|
|
630
|
+
const neverLaunched = cursor.phase === 'pending' && phase === 'identity_absent'
|
|
631
|
+
&& (launch === DELETION_MEMBER_NEVER_LAUNCHED || launch === DELETION_MEMBER_ABSENT_VERIFIED);
|
|
632
|
+
if (step !== 1 && !neverLaunched)
|
|
633
|
+
throw new TaskStateError(`task ${id} deletion member '${name}' cannot skip from '${cursor.phase}' to '${phase}' without evidence`);
|
|
634
|
+
if (launch === undefined)
|
|
635
|
+
throw new TaskStateError(`task ${id} deletion member '${name}' cannot reach '${phase}' without a launch ownership proof`);
|
|
636
|
+
if (phase === 'archive_secured' && (cursor.archive_path ?? archivePath) === undefined)
|
|
637
|
+
throw new TaskStateError(`task ${id} deletion member '${name}' cannot reach 'archive_secured' without archive evidence`);
|
|
638
|
+
cursor.phase = phase;
|
|
639
|
+
cursor.launch_id = launch;
|
|
640
|
+
if (archivePath !== undefined)
|
|
641
|
+
cursor.archive_path = archivePath;
|
|
642
|
+
cursor.updated_at = new Date().toISOString();
|
|
643
|
+
writeTask(stored);
|
|
644
|
+
return presentTaskLenient(stored);
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
function findCursorForSeat(id, stored, seat) {
|
|
648
|
+
const cursor = stored.deletion.members.find(member => member.name === seat.role_name);
|
|
649
|
+
if (cursor && seat.identity_cid
|
|
650
|
+
&& cursor.identity_cid.toLowerCase() !== seat.identity_cid.toLowerCase())
|
|
651
|
+
throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' identity CID mismatch between cursor and room seat`);
|
|
652
|
+
return cursor;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Durably register cursors for late-provisioned members before retirement
|
|
656
|
+
* begins: provisioning publishes room seats before updateTaskMembers, so the
|
|
657
|
+
* acceptance snapshot can be empty while live seats exist. Seats without a
|
|
658
|
+
* recorded identity CID are not registered here — until a CID is recorded
|
|
659
|
+
* there is no managed identity to orphan, and the room record still carries
|
|
660
|
+
* those seats through the close saga that follows.
|
|
661
|
+
*/
|
|
662
|
+
export function upsertTaskDeletionMembersFromSeats(id, seats) {
|
|
663
|
+
return withTaskLock(id, () => {
|
|
664
|
+
assertCanonicalTaskId(id);
|
|
665
|
+
const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
|
|
666
|
+
if (stored.deletion?.status !== 'pending')
|
|
667
|
+
throw new TaskStateError(`task ${id} has no pending deletion`);
|
|
668
|
+
const now = new Date().toISOString();
|
|
669
|
+
for (const seat of seats) {
|
|
670
|
+
if (!seat.identity_cid)
|
|
671
|
+
continue;
|
|
672
|
+
if (!findCursorForSeat(id, stored, seat)) {
|
|
673
|
+
stored.deletion.members.push({
|
|
674
|
+
name: seat.role_name, identity_cid: seat.identity_cid, phase: 'pending', updated_at: now,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
writeTask(stored);
|
|
679
|
+
return presentTaskLenient(stored);
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* The pre-room-delete checkpoint: import completed retirement evidence from
|
|
684
|
+
* room seats into the deletion cursors, so a crash between room-record
|
|
685
|
+
* deletion and task unlink retries from durable cursors instead of
|
|
686
|
+
* reconstructing consumed evidence. Call ONLY after closeManagedRoom has
|
|
687
|
+
* completed retirement and BEFORE cowork.deleteRoom/deleteRoomRecord.
|
|
688
|
+
*
|
|
689
|
+
* The room saga is trusted for phase jumps, but partial or corrupt retained
|
|
690
|
+
* records must not become false success: every seat must be identity_absent;
|
|
691
|
+
* a real launch requires archive evidence; an identity-less seat is accepted
|
|
692
|
+
* only via the never-launched proof.
|
|
693
|
+
*/
|
|
694
|
+
export function importTaskDeletionRetirementEvidence(id, seats) {
|
|
695
|
+
return withTaskLock(id, () => {
|
|
696
|
+
assertCanonicalTaskId(id);
|
|
697
|
+
const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
|
|
698
|
+
if (stored.deletion?.status !== 'pending')
|
|
699
|
+
throw new TaskStateError(`task ${id} has no pending deletion`);
|
|
700
|
+
const now = new Date().toISOString();
|
|
701
|
+
for (const seat of seats) {
|
|
702
|
+
const evidence = seat.retirement;
|
|
703
|
+
if (evidence?.phase !== 'identity_absent')
|
|
704
|
+
throw new TaskStateError(`task ${id} deletion cannot checkpoint member '${seat.role_name}' before completed retirement`);
|
|
705
|
+
const neverLaunched = evidence.launch_id === DELETION_MEMBER_NEVER_LAUNCHED;
|
|
706
|
+
if (!neverLaunched && evidence.archive_path === undefined)
|
|
707
|
+
throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' retirement evidence lacks its archive`);
|
|
708
|
+
if (!seat.identity_cid) {
|
|
709
|
+
if (neverLaunched)
|
|
710
|
+
continue; // provably never held a managed identity
|
|
711
|
+
throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' has retirement evidence but no identity CID`);
|
|
712
|
+
}
|
|
713
|
+
let cursor = findCursorForSeat(id, stored, seat);
|
|
714
|
+
if (!cursor) {
|
|
715
|
+
cursor = { name: seat.role_name, identity_cid: seat.identity_cid, phase: 'pending', updated_at: now };
|
|
716
|
+
stored.deletion.members.push(cursor);
|
|
717
|
+
}
|
|
718
|
+
if (DELETION_MEMBER_PHASE_ORDER[evidence.phase] < DELETION_MEMBER_PHASE_ORDER[cursor.phase])
|
|
719
|
+
continue;
|
|
720
|
+
if (cursor.launch_id !== undefined && cursor.launch_id !== evidence.launch_id)
|
|
721
|
+
throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' launch ownership proof is immutable`);
|
|
722
|
+
if (cursor.archive_path !== undefined && evidence.archive_path !== undefined
|
|
723
|
+
&& cursor.archive_path !== evidence.archive_path)
|
|
724
|
+
throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' archive evidence is immutable`);
|
|
725
|
+
cursor.phase = evidence.phase;
|
|
726
|
+
cursor.launch_id = evidence.launch_id;
|
|
727
|
+
if (evidence.archive_path !== undefined)
|
|
728
|
+
cursor.archive_path = evidence.archive_path;
|
|
729
|
+
cursor.updated_at = now;
|
|
730
|
+
}
|
|
731
|
+
writeTask(stored);
|
|
732
|
+
return presentTaskLenient(stored);
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Physically remove a deletion-pending task record. Settlement-only: callers
|
|
737
|
+
* must have completed member retirement and room cleanup first. Missing
|
|
738
|
+
* records are the idempotent already-settled outcome.
|
|
739
|
+
*/
|
|
740
|
+
export function unlinkDeletedTask(id) {
|
|
741
|
+
return withTaskLock(id, () => {
|
|
742
|
+
assertCanonicalTaskId(id);
|
|
743
|
+
let stored;
|
|
744
|
+
try {
|
|
745
|
+
stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
if (isNotFoundError(error))
|
|
749
|
+
return false;
|
|
750
|
+
throw error;
|
|
403
751
|
}
|
|
752
|
+
if (stored.deletion?.status !== 'pending')
|
|
753
|
+
throw new TaskStateError(`task ${id} has no pending deletion`);
|
|
404
754
|
try {
|
|
405
|
-
unlinkSync(
|
|
755
|
+
unlinkSync(taskPath(id));
|
|
406
756
|
}
|
|
407
757
|
catch (error) {
|
|
408
758
|
if (isNotFoundError(error))
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { TemplateDefinition, TemplateSnapshot } from './types.js';
|
|
2
|
+
import type { AgentTemplateDefinition } from '../config.js';
|
|
2
3
|
export declare function hashTemplate(t: TemplateDefinition): string;
|
|
3
|
-
export declare function snapshotTemplate(t: TemplateDefinition): TemplateSnapshot;
|
|
4
|
+
export declare function snapshotTemplate(t: TemplateDefinition, agentTemplates?: Record<string, AgentTemplateDefinition>): TemplateSnapshot;
|
|
5
|
+
/** Explicit mutation boundary immediately before Task/Room plan persistence. */
|
|
6
|
+
export declare function sealTemplateSnapshot(snapshot: TemplateSnapshot, agentTemplates: Record<string, AgentTemplateDefinition>, launchDefinitions?: Record<string, AgentTemplateDefinition>): TemplateSnapshot;
|
|
4
7
|
export declare function resolveTemplate(name: string, customTemplates: Record<string, TemplateDefinition>): TemplateDefinition | undefined;
|
|
5
8
|
export declare function listTemplates(customTemplates: Record<string, TemplateDefinition>): TemplateDefinition[];
|