@ours.network/fleet 1.2.0-nightly.80 → 1.2.0-nightly.82

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.
@@ -89,6 +89,16 @@ const PEER_WAIT_REMINDER_FINGERPRINTS = Object.freeze({
89
89
  'roles/Critic.yaml': ['998abdbc5159e8e1c26eb1ca09f9707b005ba1930bd5b7a4989c4d6893804c4a'],
90
90
  'roles/Engineer.yaml': ['a361453ad32cde08797cf90fff17a35bde1871ccea17389448b44398b45ccfa5'],
91
91
  });
92
+ // The packaged revision-11 charters before task Progress and the self-contained To review handoff.
93
+ // Untouched copies adopt the current text; edited ones stay.
94
+ const REVIEW_PACKAGE_FINGERPRINTS = Object.freeze({
95
+ 'roles/Developer.yaml': ['cd2da8c9fb4d56b08c618221e38b9c8a2568a59b7d7ee6e44d0f6f55f9dc42e9'],
96
+ 'roles/Critic.yaml': ['b2576fe4b9501581e874daa40b77e0379220c855a76d0fd80945e9fc42e08131'],
97
+ 'roles/Engineer.yaml': ['dc99a42f64c4bc2c5073917f4552a366a0ab95c24bdde9e13d66320d68edac1b'],
98
+ 'roles/LocalCoordinator.yaml': ['1a4c7613de0c374ac49ede31b312fcfd3f4a08d31316cf8dda10e46f25844e03'],
99
+ 'roles/Coordinator.yaml': ['1fa72470f37330a96debdfa6a64004280edcf318fafd5f6a5065a06ee7e87acc'],
100
+ 'agent_templates/LocalCoordinator.yaml': ['931908da43df7c85dd2b106c99c765a3aeb4a97361fa00ce110e3ac93221ce32'],
101
+ });
92
102
  const CURRENT_ROLE_DEFAULTS = new Set([
93
103
  'roles/Coordinator.yaml', 'roles/LocalCoordinator.yaml', 'roles/Developer.yaml', 'roles/Critic.yaml',
94
104
  'roles/Engineer.yaml',
@@ -221,6 +231,7 @@ export function migratePackagedRoleDefaults(configuration, options = {}, hooks =
221
231
  ...Object.keys(OWNER_CHANNEL_COORDINATOR_FINGERPRINTS),
222
232
  ...Object.keys(REREADING_CONTINUITY_LOOP_FINGERPRINTS),
223
233
  ...Object.keys(PEER_WAIT_REMINDER_FINGERPRINTS),
234
+ ...Object.keys(REVIEW_PACKAGE_FINGERPRINTS),
224
235
  ]);
225
236
  for (const relative of known) {
226
237
  const path = join(root, relative);
@@ -237,6 +248,7 @@ export function migratePackagedRoleDefaults(configuration, options = {}, hooks =
237
248
  ...(OWNER_CHANNEL_COORDINATOR_FINGERPRINTS[relative] ?? []),
238
249
  ...(REREADING_CONTINUITY_LOOP_FINGERPRINTS[relative] ?? []),
239
250
  ...(PEER_WAIT_REMINDER_FINGERPRINTS[relative] ?? []),
251
+ ...(REVIEW_PACKAGE_FINGERPRINTS[relative] ?? []),
240
252
  ].some(expected => expected !== undefined && actual === expected);
241
253
  if (recognized)
242
254
  (CURRENT_ROLE_DEFAULTS.has(relative) ? replacements : removals).push(path);
@@ -45,6 +45,8 @@ import { createCoworkAdapter } from './cowork-adapter.js';
45
45
  import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from './markdown.js';
46
46
  import { recordTaskProvisioningOutcome, TaskRoomApplicationError, TaskRoomApplicationService, } from '../application/task-room-service.js';
47
47
  import { TaskListError } from './task-lists.js';
48
+ import { appendTaskProgress, attachTaskReviewArtifact, detachTaskReviewArtifact, getTaskReview, listTaskProgress, openTaskWorkspaceFile, publicProgressEntry, publicReview, setTaskReviewMarkdown, TaskReviewError, } from './task-review.js';
49
+ import { verifiedFleetCaller } from '../fleet-command-audit.js';
48
50
  import { beginFleetAuditCollection, consumeFleetAuditCollection, recordFleetAuditFailure, recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
49
51
  import { renderAgentConfiguration } from '../lifecycle-summary.js';
50
52
  import { withFileLock } from '../atomic-file.js';
@@ -135,6 +137,28 @@ function taskRoomPublicFailure(error) {
135
137
  };
136
138
  }
137
139
  }
140
+ /** A supervisor-verified managed agent is attributed as itself; otherwise the local operator. */
141
+ function cliContributionAuthor(taskId) {
142
+ const verified = verifiedFleetCaller();
143
+ if (!verified)
144
+ return { kind: 'local_control', surface: 'cli' };
145
+ // The same identity CID the agent's MCP tools use, so retries match across both surfaces.
146
+ const cid = getTask(taskId).member_roles.find(item => item.name === verified.caller)?.identity_cid ?? bridgeIdentityCid(verified.stateDir);
147
+ return { kind: 'agent', role: verified.caller, ...(cid ? { identity_cid: cid } : {}), surface: 'cli' };
148
+ }
149
+ /** The managed runtime CID its supervisor recorded for the agent's MCP bridge, when present. */
150
+ function bridgeIdentityCid(stateDir) {
151
+ try {
152
+ const cid = JSON.parse(readFileSync(join(stateDir, '.ours-bridge', 'descriptor.json'), 'utf8')).cid;
153
+ return typeof cid === 'string' && /^[A-Fa-f0-9]{64}$/.test(cid) ? cid : undefined;
154
+ }
155
+ catch {
156
+ return undefined;
157
+ }
158
+ }
159
+ function contributionAuthorLabel(author) {
160
+ return author.kind === 'agent' ? `${author.role} (agent, ${author.surface})` : `local ${author.surface}`;
161
+ }
138
162
  function die(e) {
139
163
  const msg = e instanceof TaskRoomPublicError
140
164
  ? taskRoomPublicFailure(e).legacy : e instanceof Error ? e.message : String(e);
@@ -201,6 +225,19 @@ function dieTaskRoom(e) {
201
225
  recordFleetAuditFailure({ class: 'validation', effect: 'not_started' });
202
226
  process.exit(1);
203
227
  }
228
+ if (e instanceof TaskReviewError) {
229
+ const output = renderMarkdownFailure({
230
+ kind: e.code === 'not_found' ? 'not_found' : e.code === 'conflict' || e.code === 'idempotency_conflict' ? 'state' : 'usage',
231
+ subject: 'ours-fleet task progress/review-package',
232
+ detail: e.message,
233
+ action: e.code === 'conflict' ? 'Show the current review package and retry against it.'
234
+ : e.code === 'idempotency_conflict' ? 'Use a new --request-key for different content.'
235
+ : 'Correct the request and retry.',
236
+ });
237
+ process.stderr.write(`${output}\n`);
238
+ recordFleetAuditFailure({ class: 'validation', effect: 'not_started' });
239
+ process.exit(1);
240
+ }
204
241
  if (e instanceof TaskListError) {
205
242
  const notFound = e.code === 'list_not_found';
206
243
  const conflict = ['duplicate_name', 'reserved_name', 'default_immutable',
@@ -1028,6 +1065,86 @@ export function registerTaskCommands(parent, cOpt) {
1028
1065
  dieTaskRoom(e);
1029
1066
  }
1030
1067
  });
1068
+ taskCmd.command('progress <id>')
1069
+ .description('read the task Progress log, or append one significant milestone with --text')
1070
+ .option('--text <text>', 'milestone text to append, kept exactly as written')
1071
+ .option('--text-file <path>', 'milestone text to append, from a file')
1072
+ .option('--request-key <key>', 'retry key: repeating the same key and text returns the original entry')
1073
+ .option('--json', 'JSON output')
1074
+ .action((id, opts) => {
1075
+ try {
1076
+ if (opts.text !== undefined && opts.textFile !== undefined)
1077
+ throw new Error('use --text or --text-file, not both');
1078
+ const text = opts.textFile !== undefined ? readFileSync(opts.textFile, 'utf8') : opts.text;
1079
+ if (text === undefined) {
1080
+ if (opts.requestKey !== undefined)
1081
+ throw new Error('--request-key applies only when appending');
1082
+ const progress = listTaskProgress(id).map(publicProgressEntry);
1083
+ if (opts.json) {
1084
+ console.log(JSON.stringify({ schema_version: 1, task_id: id, progress }, null, 2));
1085
+ return;
1086
+ }
1087
+ console.log(progress.length ? progress.map(entry => `- ${entry.at} — ${contributionAuthorLabel(entry.author)}\n\n${entry.text}\n`).join('\n')
1088
+ : `Task ${id} has no progress entries.`);
1089
+ return;
1090
+ }
1091
+ const result = appendTaskProgress(id, { text, requestKey: opts.requestKey, author: cliContributionAuthor(id) });
1092
+ if (opts.json) {
1093
+ console.log(JSON.stringify({ schema_version: 1, task_id: id, entry: publicProgressEntry(result.entry), replayed: result.replayed }, null, 2));
1094
+ return;
1095
+ }
1096
+ console.log(result.replayed ? `Progress entry ${result.entry.id} was already recorded.` : `Progress entry ${result.entry.id} added to task ${id}.`);
1097
+ }
1098
+ catch (e) {
1099
+ if (opts.json)
1100
+ die(e);
1101
+ dieTaskRoom(e);
1102
+ }
1103
+ });
1104
+ taskCmd.command('review-package <id>')
1105
+ .description('show or prepare the task To review package (Markdown and retained files)')
1106
+ .option('--markdown <text>', 'replace the review Markdown')
1107
+ .option('--markdown-file <path>', 'replace the review Markdown from a file')
1108
+ .option('--attach <path>', 'retain a file from the task workspace in the package (repeatable)', (value, all) => [...all, value], [])
1109
+ .option('--detach <artifact-id>', 'remove a file from the package (repeatable)', (value, all) => [...all, value], [])
1110
+ .option('--request-key <key>', 'retry key for a single --attach: repeating it with the same file returns the original artifact')
1111
+ .option('--json', 'JSON output')
1112
+ .action(async (id, opts) => {
1113
+ try {
1114
+ if (opts.markdown !== undefined && opts.markdownFile !== undefined)
1115
+ throw new Error('use --markdown or --markdown-file, not both');
1116
+ if (opts.requestKey !== undefined && opts.attach.length !== 1)
1117
+ throw new Error('--request-key needs exactly one --attach');
1118
+ const markdown = opts.markdownFile !== undefined ? readFileSync(opts.markdownFile, 'utf8') : opts.markdown;
1119
+ const author = markdown !== undefined || opts.attach.length || opts.detach.length ? cliContributionAuthor(id) : undefined;
1120
+ const attached = [];
1121
+ if (markdown !== undefined)
1122
+ setTaskReviewMarkdown(id, { markdown, expectedMarkdown: getTaskReview(id)?.markdown ?? '', author: author });
1123
+ for (const artifactId of opts.detach)
1124
+ detachTaskReviewArtifact(id, artifactId, { author: author });
1125
+ for (const path of opts.attach) {
1126
+ const source = await openTaskWorkspaceFile(getTask(id), path);
1127
+ attached.push(await attachTaskReviewArtifact(id, { name: source.name, source, author: author, requestKey: opts.requestKey }));
1128
+ }
1129
+ const review = getTaskReview(id);
1130
+ if (opts.json) {
1131
+ console.log(JSON.stringify({ schema_version: 1, task_id: id, review: review ? publicReview(review) : null }, null, 2));
1132
+ return;
1133
+ }
1134
+ if (!review) {
1135
+ console.log(`Task ${id} has no To review package yet.`);
1136
+ return;
1137
+ }
1138
+ const files = review.artifacts.map(artifact => `- \`${artifact.id}\` ${artifact.name} (${artifact.mime}, ${artifact.size} bytes)`).join('\n');
1139
+ console.log(`## To review — task ${id}\n\n${review.markdown || '_No review Markdown yet._'}\n\n### Files\n\n${files || '_No files._'}`
1140
+ + (attached.some(item => item.replayed) ? '\n\n(The attached file was already recorded.)' : ''));
1141
+ }
1142
+ catch (e) {
1143
+ if (opts.json)
1144
+ die(e);
1145
+ dieTaskRoom(e);
1146
+ }
1147
+ });
1031
1148
  cOpt(taskCmd.command('done <id>'))
1032
1149
  .description('complete a task')
1033
1150
  .option('--summary <text>', 'completion summary')
@@ -15,6 +15,7 @@ import { deleteRoomRecord, getRoomRecord, listRoomRecords } from './room-state.j
15
15
  import { acquireLaunchSnapshotLock, releaseLaunchSnapshotForDeletingTask } from './launch-snapshot.js';
16
16
  import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
17
17
  import { advanceTaskDeletionMember, settleAbsentTaskDeletionMember, beginTaskDeletionIntent, completeTaskDeletionReceipt, ensureTaskDeletionReceipt, getDeletingTask, importTaskDeletionRetirementEvidence, setTaskDeletionError, TaskStateError, beginTaskArchiveCleanup, unlinkDeletedTask, upsertTaskDeletionMembersFromSeats, } from './task-state.js';
18
+ import { deleteTaskReviewStore } from './task-review.js';
18
19
  export { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
19
20
  import { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
20
21
  /**
@@ -266,6 +267,8 @@ export async function settleTaskDeletion(input) {
266
267
  collectWorkspaceArchives(task.workspace);
267
268
  deleteWorkspace(task.workspace, 'task', taskId);
268
269
  }
270
+ // Retained To review files go with the task; the pending deletion already refuses new attachments.
271
+ deleteTaskReviewStore(taskId);
269
272
  unlinkDeletedTask(taskId);
270
273
  }
271
274
  catch (error) {
@@ -0,0 +1,113 @@
1
+ import type { TaskContributionAuthor, TaskProgressEntry, TaskRecord, TaskReview, TaskReviewArtifact } from './types.js';
2
+ export declare const PROGRESS_TEXT_MAX = 4000;
3
+ export declare const PROGRESS_MAX_ENTRIES = 200;
4
+ export declare const REVIEW_MARKDOWN_MAX = 50000;
5
+ export declare const REVIEW_MAX_ARTIFACTS = 20;
6
+ export declare const REVIEW_ARTIFACT_MAX_BYTES: number;
7
+ export declare class TaskReviewError extends Error {
8
+ readonly code: 'invalid_request' | 'idempotency_conflict' | 'limit_exceeded' | 'conflict' | 'not_found';
9
+ constructor(code: 'invalid_request' | 'idempotency_conflict' | 'limit_exceeded' | 'conflict' | 'not_found', message: string);
10
+ }
11
+ export declare function reviewArtifactMime(name: string): string;
12
+ export declare function checkArtifactName(name: string): string;
13
+ export declare function listTaskProgress(taskId: string): TaskProgressEntry[];
14
+ /**
15
+ * Append one milestone. Text is kept exactly as written; whitespace-only text is
16
+ * rejected. A retry with the same author-scoped request_key returns the original
17
+ * entry; the same key with different text is a conflict.
18
+ */
19
+ export declare function appendTaskProgress(taskId: string, input: {
20
+ text: string;
21
+ author: TaskContributionAuthor;
22
+ requestKey?: string;
23
+ }): {
24
+ entry: TaskProgressEntry;
25
+ replayed: boolean;
26
+ };
27
+ export declare function getTaskReview(taskId: string): TaskReview | undefined;
28
+ /**
29
+ * Replace the review Markdown. `expectedMarkdown` is what the writer last read
30
+ * ('' when none); a different current value is a stale edit. Re-sending the
31
+ * value already stored succeeds, so retries are safe.
32
+ */
33
+ export declare function setTaskReviewMarkdown(taskId: string, input: {
34
+ markdown: string;
35
+ expectedMarkdown: string;
36
+ author: TaskContributionAuthor;
37
+ }): TaskReview;
38
+ export declare const reviewStoreRoot: () => string;
39
+ export declare function reviewStoreDirectory(taskId: string): string;
40
+ /** A file being read; `verify` fails when it changed since it was opened. */
41
+ export type Source = {
42
+ size: number;
43
+ body: ReadableStream<Uint8Array>;
44
+ close(): Promise<void>;
45
+ verify?(): Promise<void>;
46
+ };
47
+ type Stored = {
48
+ id: string;
49
+ size: number;
50
+ sha256: string;
51
+ };
52
+ /**
53
+ * Attach one file to the review package: reserve under the task lock, copy
54
+ * into the private store, then commit metadata under the lock again; any
55
+ * failure removes the copy. A retry with the same author-scoped request_key
56
+ * is compared by content without storing another copy: identical content
57
+ * returns the original artifact, different content is a conflict. Without a
58
+ * key an interrupted call may or may not have attached: read the package
59
+ * before retrying.
60
+ */
61
+ export declare function attachTaskReviewArtifact(taskId: string, input: {
62
+ name: string;
63
+ source: Source;
64
+ author: TaskContributionAuthor;
65
+ requestKey?: string;
66
+ signal?: AbortSignal;
67
+ /** Test seam: runs after the bytes are durable, before the record references them. */
68
+ beforeCommit?(stored: Stored): void;
69
+ /** Test seam: runs after a keyed retry was read, before it is confirmed under the lock. */
70
+ beforeReplay?(): void;
71
+ }): Promise<{
72
+ artifact: TaskReviewArtifact;
73
+ replayed: boolean;
74
+ }>;
75
+ /** Remove a file from the package; repeating it after success reports not found. */
76
+ export declare function detachTaskReviewArtifact(taskId: string, artifactId: string, input: {
77
+ author: TaskContributionAuthor;
78
+ }): TaskReviewArtifact;
79
+ /** Bytes of a referenced artifact, integrity-checked; only the owning task can name it. */
80
+ export declare function readTaskReviewArtifact(taskId: string, artifactId: string): Promise<{
81
+ artifact: TaskReviewArtifact;
82
+ bytes: Buffer;
83
+ }>;
84
+ /** Task deletion: remove every retained review file of the task. */
85
+ export declare function deleteTaskReviewStore(taskId: string): void;
86
+ /** True when `path` names an entry strictly inside `root`, compared by path components. */
87
+ export declare function insideDirectory(root: string, path: string): boolean;
88
+ /** The task's owned workspace, validated against its ownership marker. */
89
+ export declare function taskWorkspaceRoot(task: TaskRecord): string;
90
+ /**
91
+ * Open a file inside the task workspace in this process's own OS context (the
92
+ * CLI). Every component below the pinned workspace is opened without
93
+ * following symlinks; the file must be one regular, singly-linked file.
94
+ */
95
+ export declare function openTaskWorkspaceFile(task: TaskRecord, input: string, cwd?: string): Promise<Source & {
96
+ name: string;
97
+ }>;
98
+ /** Size of the store, for diagnostics and tests. */
99
+ export declare function reviewStoreFiles(taskId: string): string[];
100
+ /** Wire shapes: retry keys stay internal. */
101
+ export declare function publicProgressEntry(entry: TaskProgressEntry): Omit<TaskProgressEntry, 'request_key'>;
102
+ export declare function publicReview(review: TaskReview): TaskReview;
103
+ /** Compact summary for list and detail payloads; bodies are fetched separately. */
104
+ export declare function taskContributionSummary(task: TaskRecord): {
105
+ progress_count: number;
106
+ review: {
107
+ has_markdown: boolean;
108
+ artifact_count: number;
109
+ revision: number;
110
+ updated_at?: string;
111
+ };
112
+ };
113
+ export {};