@ours.network/fleet 2.2.3 → 2.2.5

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.
@@ -16,8 +16,7 @@ export class TaskNotificationProducer {
16
16
  offset = 0;
17
17
  stopped = false;
18
18
  shutdown = new AbortController();
19
- bootstrapped = false;
20
- bootstrapAt = 0;
19
+ bootstrapAttempted = false;
21
20
  constructor(config, warn = () => { }, request = fetch,
22
21
  /** Existing tasks and their layout room CIDs, registered once per start before own-room OFF can be chosen. */
23
22
  missions) {
@@ -68,21 +67,14 @@ export class TaskNotificationProducer {
68
67
  // Deletion first: a retired task's notifications go before any other delivery work in this pass.
69
68
  if (convertRetiredTaskNotifications() || await drainMissionCleanup(this.config, this.warn, this.request))
70
69
  this.retry();
71
- if (this.missions && !this.bootstrapped && !this.stopped) {
72
- // A failed run waits 30s: unavailable inventory or a refused registration needs repair, not a tight loop.
73
- const wait = this.bootstrapAt - Date.now();
74
- if (wait > 0)
75
- this.retry(wait);
76
- else {
77
- try {
78
- await bootstrapMissionRooms(this.config, this.missions.tasks, this.missions.layoutRooms, this.request);
79
- this.bootstrapped = true;
80
- }
81
- catch {
82
- this.warn('Mission room registration pending; own-room message preference stays unavailable');
83
- this.bootstrapAt = Date.now() + 30_000;
84
- this.retry(30_000);
85
- }
70
+ if (this.missions && !this.bootstrapAttempted && !this.stopped) {
71
+ // One startup attempt. Association failure neither blocks task events nor schedules another registration.
72
+ this.bootstrapAttempted = true;
73
+ try {
74
+ await bootstrapMissionRooms(this.config, this.missions.tasks, this.missions.layoutRooms, this.request);
75
+ }
76
+ catch {
77
+ this.warn('Mission room registration failed; own-room message preference may be unavailable');
86
78
  }
87
79
  }
88
80
  const pending = pendingTaskNotifications(this.warn);
@@ -99,6 +99,16 @@ const REVIEW_PACKAGE_FINGERPRINTS = Object.freeze({
99
99
  'roles/Coordinator.yaml': ['1fa72470f37330a96debdfa6a64004280edcf318fafd5f6a5065a06ee7e87acc'],
100
100
  'agent_templates/LocalCoordinator.yaml': ['931908da43df7c85dd2b106c99c765a3aeb4a97361fa00ce110e3ac93221ce32'],
101
101
  });
102
+ // The packaged revision-12 charters before the 400-character Progress rules, the single Progress
103
+ // writer and the current-request To review lifecycle with clear. Untouched copies adopt the current
104
+ // text; edited ones stay.
105
+ const CURRENT_REQUEST_FINGERPRINTS = Object.freeze({
106
+ 'roles/Developer.yaml': ['684b4cb278b92746ba3abe29523adb9ab585e0831f8519b17bc17f41a3e992a4'],
107
+ 'roles/Critic.yaml': ['b5b1d00074e6540160b14da256609b42600fa7d94ba776f8d5e7b42771669c41'],
108
+ 'roles/Engineer.yaml': ['79991291c354229a71cdbd7191ad132ea91499b5326b1386c7abe8ce1444f714'],
109
+ 'roles/LocalCoordinator.yaml': ['0fdbb9b56fedcf1897ad8044d6d88eaefcd5ccc48176860da3f7a1efe22da51d'],
110
+ 'roles/Coordinator.yaml': ['c9038f3e33068058884e2ae52f6d569c22c4d56fcdb345e9381866c6de38d9a9'],
111
+ });
102
112
  const CURRENT_ROLE_DEFAULTS = new Set([
103
113
  'roles/Coordinator.yaml', 'roles/LocalCoordinator.yaml', 'roles/Developer.yaml', 'roles/Critic.yaml',
104
114
  'roles/Engineer.yaml',
@@ -232,6 +242,7 @@ export function migratePackagedRoleDefaults(configuration, options = {}, hooks =
232
242
  ...Object.keys(REREADING_CONTINUITY_LOOP_FINGERPRINTS),
233
243
  ...Object.keys(PEER_WAIT_REMINDER_FINGERPRINTS),
234
244
  ...Object.keys(REVIEW_PACKAGE_FINGERPRINTS),
245
+ ...Object.keys(CURRENT_REQUEST_FINGERPRINTS),
235
246
  ]);
236
247
  for (const relative of known) {
237
248
  const path = join(root, relative);
@@ -249,6 +260,7 @@ export function migratePackagedRoleDefaults(configuration, options = {}, hooks =
249
260
  ...(REREADING_CONTINUITY_LOOP_FINGERPRINTS[relative] ?? []),
250
261
  ...(PEER_WAIT_REMINDER_FINGERPRINTS[relative] ?? []),
251
262
  ...(REVIEW_PACKAGE_FINGERPRINTS[relative] ?? []),
263
+ ...(CURRENT_REQUEST_FINGERPRINTS[relative] ?? []),
252
264
  ].some(expected => expected !== undefined && actual === expected);
253
265
  if (recognized)
254
266
  (CURRENT_ROLE_DEFAULTS.has(relative) ? replacements : removals).push(path);
@@ -39,13 +39,13 @@ function cliAnonymousOverride(argv) {
39
39
  throw new Error('--anonymous and --no-anonymous are mutually exclusive');
40
40
  return enabled ? true : disabled ? false : undefined;
41
41
  }
42
- import { getTask, getDeletingTask, TaskStateError, } from './task-state.js';
42
+ import { getTask, getDeletingTask, TaskStateError, reviewRevision, } from './task-state.js';
43
43
  import { getRoomRecord, RoomStateError, } from './room-state.js';
44
44
  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';
48
+ import { appendTaskProgress, attachTaskReviewArtifact, clearTaskReview, detachTaskReviewArtifact, getTaskReview, listTaskProgress, openTaskWorkspaceFile, publicProgressEntry, publicReview, setTaskReviewMarkdown, TaskReviewError, } from './task-review.js';
49
49
  import { verifiedFleetCaller } from '../fleet-command-audit.js';
50
50
  import { beginFleetAuditCollection, consumeFleetAuditCollection, recordFleetAuditFailure, recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
51
51
  import { renderAgentConfiguration } from '../lifecycle-summary.js';
@@ -1108,6 +1108,8 @@ export function registerTaskCommands(parent, cOpt) {
1108
1108
  .option('--attach <path>', 'retain a file from the task workspace in the package (repeatable)', (value, all) => [...all, value], [])
1109
1109
  .option('--detach <artifact-id>', 'remove a file from the package (repeatable)', (value, all) => [...all, value], [])
1110
1110
  .option('--request-key <key>', 'retry key for a single --attach: repeating it with the same file returns the original artifact')
1111
+ .option('--clear', 'remove the resolved request: all Markdown and files, replaced by --markdown[-file] if given (needs --expected-revision)')
1112
+ .option('--expected-revision <n>', 'package revision you last read; --clear is refused if it changed since')
1111
1113
  .option('--json', 'JSON output')
1112
1114
  .action(async (id, opts) => {
1113
1115
  try {
@@ -1115,10 +1117,22 @@ export function registerTaskCommands(parent, cOpt) {
1115
1117
  throw new Error('use --markdown or --markdown-file, not both');
1116
1118
  if (opts.requestKey !== undefined && opts.attach.length !== 1)
1117
1119
  throw new Error('--request-key needs exactly one --attach');
1120
+ if (!!opts.clear !== (opts.expectedRevision !== undefined))
1121
+ throw new TaskReviewError('invalid_request', '--clear and --expected-revision go together');
1122
+ if (opts.clear && opts.detach.length)
1123
+ throw new TaskReviewError('invalid_request', '--clear already removes every file; drop --detach');
1124
+ if (opts.clear && opts.attach.length)
1125
+ throw new TaskReviewError('invalid_request', '--clear cannot attach in the same call; attach the next request\'s files after reading the package again');
1126
+ const expectedRevision = opts.expectedRevision !== undefined ? Number(opts.expectedRevision) : undefined;
1127
+ if (expectedRevision !== undefined && (!/^\d+$/.test(opts.expectedRevision) || !Number.isSafeInteger(expectedRevision)))
1128
+ throw new TaskReviewError('invalid_request', '--expected-revision must be a non-negative integer');
1118
1129
  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;
1130
+ const author = markdown !== undefined || opts.attach.length || opts.detach.length || opts.clear ? cliContributionAuthor(id) : undefined;
1120
1131
  const attached = [];
1121
- if (markdown !== undefined)
1132
+ // A clear with Markdown replaces the request in one fenced change; nothing can land in between.
1133
+ if (opts.clear)
1134
+ clearTaskReview(id, { expectedRevision: expectedRevision, author: author, markdown });
1135
+ else if (markdown !== undefined)
1122
1136
  setTaskReviewMarkdown(id, { markdown, expectedMarkdown: getTaskReview(id)?.markdown ?? '', author: author });
1123
1137
  for (const artifactId of opts.detach)
1124
1138
  detachTaskReviewArtifact(id, artifactId, { author: author });
@@ -1136,7 +1150,7 @@ export function registerTaskCommands(parent, cOpt) {
1136
1150
  return;
1137
1151
  }
1138
1152
  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._'}`
1153
+ console.log(`## To review — task ${id} (revision ${reviewRevision(review)})\n\n${review.markdown || '_No review Markdown yet._'}\n\n### Files\n\n${files || '_No files._'}`
1140
1154
  + (attached.some(item => item.replayed) ? '\n\n(The attached file was already recorded.)' : ''));
1141
1155
  }
1142
1156
  catch (e) {
@@ -30,7 +30,7 @@ export declare class RoomLayoutService {
30
30
  supervisor(id?: string, templates?: Record<string, AgentTemplateDefinition>): NativeLayoutSupervisor;
31
31
  private owner;
32
32
  private engine;
33
- /** A task's layout rooms are registered with the notification service before the Owner can join them. */
33
+ /** A task's layout rooms attempt notification registration before Owner admission. */
34
34
  private beforeOwner;
35
35
  open(id: string, cleanup?: boolean): RoomLayout;
36
36
  /** `scope` lets an owner (e.g. a Task) snapshot an adjusted definition and pin agent working directories. */
@@ -180,7 +180,7 @@ export class RoomLayoutService {
180
180
  assertLayoutFile(path);
181
181
  return new RoomLayout(path, () => createCoworkAdapter(), this.supervisor(id), cleanup ? undefined : this.owner(), this.beforeOwner(id));
182
182
  }
183
- /** A task's layout rooms are registered with the notification service before the Owner can join them. */
183
+ /** A task's layout rooms attempt notification registration before Owner admission. */
184
184
  beforeOwner(id) {
185
185
  const taskId = /^task-([0-9a-z]{9}[0-9a-f]{8})$/.exec(id)?.[1];
186
186
  return taskId ? async (roomCid) => (await import('../notifications/mission-ownership.js')).associateRoomNotifications(taskId, roomCid) : undefined;
@@ -104,12 +104,12 @@ export declare class RoomLayout {
104
104
  private file;
105
105
  private supervisor;
106
106
  private owner?;
107
- /** Runs before the Owner may join each room; a failure stops here and the next pass retries it. */
107
+ /** Best-effort notification registration before the Owner joins each room. */
108
108
  private beforeOwner?;
109
109
  private coworkAdapter?;
110
110
  private readonly coworkFactory;
111
111
  constructor(file: string, cowork: CoworkAdapter | (() => CoworkAdapter), supervisor: LayoutSupervisor, owner?: LayoutOwner | undefined,
112
- /** Runs before the Owner may join each room; a failure stops here and the next pass retries it. */
112
+ /** Best-effort notification registration before the Owner joins each room. */
113
113
  beforeOwner?: ((roomCid: string) => Promise<void>) | undefined);
114
114
  private get cowork();
115
115
  snapshot(): RoomLayoutState;
@@ -43,7 +43,7 @@ export class RoomLayout {
43
43
  coworkAdapter;
44
44
  coworkFactory;
45
45
  constructor(file, cowork, supervisor, owner,
46
- /** Runs before the Owner may join each room; a failure stops here and the next pass retries it. */
46
+ /** Best-effort notification registration before the Owner joins each room. */
47
47
  beforeOwner) {
48
48
  this.file = file;
49
49
  this.supervisor = supervisor;
@@ -165,8 +165,14 @@ export class RoomLayout {
165
165
  activation_requirements: !spec.members.length && s.owner ? [{ role: s.owner.role, count: 1 }]
166
166
  : [...new Set(spec.members.map(roleFor))].map(role => ({ role, count: spec.members.filter(m => roleFor(m) === role).length })),
167
167
  }), native => { room.native = native; s.order.push(key); });
168
- if (s.owner && !room.owner_ready && this.beforeOwner)
169
- await this.beforeOwner(room.native.identity_cid);
168
+ if (s.owner && !room.owner_ready && this.beforeOwner) {
169
+ try {
170
+ await this.beforeOwner(room.native.identity_cid);
171
+ }
172
+ catch {
173
+ console.warn('Mission room notification registration failed; continuing room admission with ordinary notifications');
174
+ }
175
+ }
170
176
  if (s.owner && !room.owner_ready)
171
177
  await this.mutation(s, `owner:${key}`, async () => {
172
178
  await this.cowork.acceptInvite(room.native.room_id, this.owner.invite, { role: s.owner.role, expected_cid: s.owner.cid });
@@ -1,5 +1,11 @@
1
1
  import type { TaskContributionAuthor, TaskProgressEntry, TaskRecord, TaskReview, TaskReviewArtifact } from './types.js';
2
- export declare const PROGRESS_TEXT_MAX = 4000;
2
+ /**
3
+ * Maximum length of a new Progress entry, in Unicode code points of the exact
4
+ * submitted text (no normalization or trimming). A combining sequence or a
5
+ * joined emoji counts as each of its code points. Stored entries are not
6
+ * re-checked, so older, longer entries still read and display.
7
+ */
8
+ export declare const PROGRESS_TEXT_MAX = 400;
3
9
  export declare const PROGRESS_MAX_ENTRIES = 200;
4
10
  export declare const REVIEW_MARKDOWN_MAX = 50000;
5
11
  export declare const REVIEW_MAX_ARTIFACTS = 20;
@@ -15,6 +21,8 @@ export declare function listTaskProgress(taskId: string): TaskProgressEntry[];
15
21
  export declare function taskProgressSnapshot(taskId: string): Array<Omit<TaskProgressEntry, 'request_key'> & {
16
22
  unread: boolean;
17
23
  }>;
24
+ /** Progress length in Unicode code points: a character outside the BMP counts once, not as two UTF-16 units. */
25
+ export declare function progressTextLength(text: string): number;
18
26
  /**
19
27
  * Append one milestone. Text is kept exactly as written; whitespace-only text is
20
28
  * rejected. A retry with the same author-scoped request_key returns the original
@@ -39,6 +47,25 @@ export declare function setTaskReviewMarkdown(taskId: string, input: {
39
47
  expectedMarkdown: string;
40
48
  author: TaskContributionAuthor;
41
49
  }): TaskReview;
50
+ /**
51
+ * Clear the resolved To review request: Markdown and every attached file, in
52
+ * one change fenced by the whole package revision the writer last read. Any
53
+ * Markdown edit, attach or detach since then is a conflict and nothing is
54
+ * removed. Clearing an already empty package at the expected revision succeeds
55
+ * unchanged; after a successful clear, read the package again before retrying.
56
+ * Like any change, a clear gets the next revision, so read markers, unread and
57
+ * push behave as for any update. Only the files this change removed are
58
+ * deleted; a file attached after it stays. With `markdown`, the same change
59
+ * writes the next request, so no other writer can slip in between.
60
+ */
61
+ export declare function clearTaskReview(taskId: string, input: {
62
+ expectedRevision: number;
63
+ author: TaskContributionAuthor;
64
+ markdown?: string;
65
+ }): {
66
+ review: TaskReview | undefined;
67
+ removed: TaskReviewArtifact[];
68
+ };
42
69
  export declare const reviewStoreRoot: () => string;
43
70
  export declare function reviewStoreDirectory(taskId: string): string;
44
71
  /** A file being read; `verify` fails when it changed since it was opened. */
@@ -14,7 +14,13 @@ import { stateRoot } from '../paths.js';
14
14
  import { MAX_FILE_BYTES, openDirectory } from '../file-delivery/reader.js';
15
15
  import { contributionUnread, getTask, reviewRevision, TaskStateError, updateTaskContribution } from './task-state.js';
16
16
  import { assertWorkspacePresent } from './workspace.js';
17
- export const PROGRESS_TEXT_MAX = 4_000;
17
+ /**
18
+ * Maximum length of a new Progress entry, in Unicode code points of the exact
19
+ * submitted text (no normalization or trimming). A combining sequence or a
20
+ * joined emoji counts as each of its code points. Stored entries are not
21
+ * re-checked, so older, longer entries still read and display.
22
+ */
23
+ export const PROGRESS_TEXT_MAX = 400;
18
24
  export const PROGRESS_MAX_ENTRIES = 200;
19
25
  export const REVIEW_MARKDOWN_MAX = 50_000;
20
26
  export const REVIEW_MAX_ARTIFACTS = 20;
@@ -83,6 +89,13 @@ export function taskProgressSnapshot(taskId) {
83
89
  ...publicProgressEntry(entry), unread: !!reads && !seen.has(entry.id),
84
90
  }));
85
91
  }
92
+ /** Progress length in Unicode code points: a character outside the BMP counts once, not as two UTF-16 units. */
93
+ export function progressTextLength(text) {
94
+ let length = 0;
95
+ for (const _ of text)
96
+ length++;
97
+ return length;
98
+ }
86
99
  /**
87
100
  * Append one milestone. Text is kept exactly as written; whitespace-only text is
88
101
  * rejected. A retry with the same author-scoped request_key returns the original
@@ -92,8 +105,10 @@ export function appendTaskProgress(taskId, input) {
92
105
  assertTaskId(taskId);
93
106
  if (typeof input.text !== 'string' || !input.text.trim())
94
107
  throw new TaskReviewError('invalid_request', 'progress text is required');
95
- if (input.text.length > PROGRESS_TEXT_MAX)
96
- throw new TaskReviewError('invalid_request', `progress text maximum is ${PROGRESS_TEXT_MAX} characters`);
108
+ const length = progressTextLength(input.text);
109
+ if (length > PROGRESS_TEXT_MAX)
110
+ throw new TaskReviewError('invalid_request', `progress text is ${length} Unicode characters (code points); the maximum is ${PROGRESS_TEXT_MAX}. `
111
+ + 'Nothing was saved. Keep one fact per entry and shorten it, or record distinct facts as separate entries.');
97
112
  checkRequestKey(input.requestKey);
98
113
  return updateTaskContribution(taskId, task => {
99
114
  const progress = task.progress ?? [];
@@ -149,6 +164,44 @@ export function setTaskReviewMarkdown(taskId, input) {
149
164
  return { value: task.review, changed: true };
150
165
  });
151
166
  }
167
+ /**
168
+ * Clear the resolved To review request: Markdown and every attached file, in
169
+ * one change fenced by the whole package revision the writer last read. Any
170
+ * Markdown edit, attach or detach since then is a conflict and nothing is
171
+ * removed. Clearing an already empty package at the expected revision succeeds
172
+ * unchanged; after a successful clear, read the package again before retrying.
173
+ * Like any change, a clear gets the next revision, so read markers, unread and
174
+ * push behave as for any update. Only the files this change removed are
175
+ * deleted; a file attached after it stays. With `markdown`, the same change
176
+ * writes the next request, so no other writer can slip in between.
177
+ */
178
+ export function clearTaskReview(taskId, input) {
179
+ const dir = reviewStoreDirectory(taskId);
180
+ if (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0)
181
+ throw new TaskReviewError('invalid_request', 'expected revision must be a non-negative integer');
182
+ const markdown = input.markdown ?? '';
183
+ if (typeof markdown !== 'string')
184
+ throw new TaskReviewError('invalid_request', 'markdown must be a string');
185
+ if (markdown.length > REVIEW_MARKDOWN_MAX)
186
+ throw new TaskReviewError('invalid_request', `review markdown maximum is ${REVIEW_MARKDOWN_MAX} characters`);
187
+ const result = updateTaskContribution(taskId, task => {
188
+ if (reviewRevision(task.review) !== input.expectedRevision)
189
+ throw new TaskReviewError('conflict', 'The review package changed elsewhere. Read it again before clearing it.');
190
+ const empty = !task.review || (!task.review.markdown && !task.review.artifacts.length);
191
+ if (empty && !markdown)
192
+ return { value: { review: task.review, removed: [] }, changed: false };
193
+ const removed = task.review?.artifacts ?? [];
194
+ task.review = { revision: nextRevision(task.review), markdown, updated_at: new Date().toISOString(),
195
+ updated_by: input.author, artifacts: [] };
196
+ return { value: { review: task.review, removed }, changed: true };
197
+ });
198
+ for (const artifact of result.removed)
199
+ try {
200
+ removeStoredFile(dir, artifact.id);
201
+ }
202
+ catch { /* an unreferenced copy is swept later */ }
203
+ return result;
204
+ }
152
205
  // ── Review artifact store ──────────────────────────────────────────────
153
206
  export const reviewStoreRoot = () => join(stateRoot(), 'task-review-artifacts');
154
207
  export function reviewStoreDirectory(taskId) {
@@ -9,7 +9,7 @@ export interface TaskToolBinding {
9
9
  /** Launched as a task-owned temporary member (layout participant). */
10
10
  taskOwned: boolean;
11
11
  }
12
- export declare const TASK_TOOL_NAMES: readonly ["fleet_task_progress_list", "fleet_task_progress_append", "fleet_task_review_show", "fleet_task_review_set", "fleet_task_review_detach"];
12
+ export declare const TASK_TOOL_NAMES: readonly ["fleet_task_progress_list", "fleet_task_progress_append", "fleet_task_review_show", "fleet_task_review_set", "fleet_task_review_clear", "fleet_task_review_detach"];
13
13
  export declare const PROGRESS_PAGE_MAX = 50;
14
14
  /** One chronological page of the log; `next_after` continues after the last returned entry. */
15
15
  export declare function progressPage(entries: TaskProgressEntry[], input: {
@@ -1,9 +1,9 @@
1
1
  import { z } from 'zod';
2
- import { appendTaskProgress, detachTaskReviewArtifact, getTaskReview, listTaskProgress, publicProgressEntry, publicReview, setTaskReviewMarkdown, TaskReviewError, PROGRESS_TEXT_MAX, REVIEW_MARKDOWN_MAX, } from './task-review.js';
2
+ import { appendTaskProgress, detachTaskReviewArtifact, getTaskReview, listTaskProgress, publicProgressEntry, publicReview, setTaskReviewMarkdown, clearTaskReview, TaskReviewError, PROGRESS_TEXT_MAX, REVIEW_MARKDOWN_MAX, } from './task-review.js';
3
3
  import { getTask, TaskStateError } from './task-state.js';
4
4
  export const TASK_TOOL_NAMES = Object.freeze([
5
5
  'fleet_task_progress_list', 'fleet_task_progress_append',
6
- 'fleet_task_review_show', 'fleet_task_review_set', 'fleet_task_review_detach',
6
+ 'fleet_task_review_show', 'fleet_task_review_set', 'fleet_task_review_clear', 'fleet_task_review_detach',
7
7
  ]);
8
8
  /** Compact JSON: a result is carried twice (text and structured) inside one bounded bridge frame. */
9
9
  function result(value) {
@@ -97,11 +97,13 @@ export function registerTaskTools(server, binding, admit) {
97
97
  },
98
98
  }, async (args) => run(bound => ({ task_id: bound.taskId, ...progressPage(listTaskProgress(bound.taskId), args) })));
99
99
  server.registerTool('fleet_task_progress_append', {
100
- description: 'Append ONE significant, verified milestone to your task\'s Progress log (e.g. root cause found, PR opened with link, '
101
- + 'measured result with its scope, final deliverable ready). Not for routine status, handoffs or review chatter. '
100
+ description: 'Append ONE significant, verified fact the Owner should know to your task\'s Progress log: delivered behavior, root cause, '
101
+ + 'measured result with a valid comparison, PR or publication, important blocker, changed scope, or a required Owner decision '
102
+ + '(e.g. "Owner review required: choose the deployment option in To review."). Lead with the result and name the actor; '
103
+ + 'one fact per entry, in the task\'s language. Not for routine status, handoffs or review chatter. '
102
104
  + 'Entries are permanent and shown to the Owner as written. Repeating a call with the same request_key and text returns the original entry.',
103
105
  inputSchema: {
104
- text: z.string().describe(`Free-form milestone text, kept exactly as written (max ${PROGRESS_TEXT_MAX} characters).`),
106
+ text: z.string().describe(`Entry text, kept exactly as written: at most ${PROGRESS_TEXT_MAX} Unicode characters (code points); longer text is refused, not cut.`),
105
107
  request_key: z.string().optional().describe('Stable key for this milestone so a retried call cannot add it twice (e.g. "pr-opened-123").'),
106
108
  },
107
109
  }, async (args) => run((bound, author) => {
@@ -116,8 +118,9 @@ export function registerTaskTools(server, binding, admit) {
116
118
  return { task_id: bound.taskId, review: review ? publicReview(review) : null };
117
119
  }));
118
120
  server.registerTool('fleet_task_review_set', {
119
- description: 'Replace the Markdown of your task\'s To review package: the self-contained material the Owner reviews without reading the room or task description '
120
- + '(outcome, clickable PR/spec links, validation and CI, independent verdict, limits, decisions needed). '
121
+ description: 'Replace the Markdown of your task\'s To review package: the one current request the Owner reviews without reading the room or task description '
122
+ + '(final acceptance: outcome, clickable PR/spec links, validation and CI, independent verdict, limits; or one Owner decision with its options). '
123
+ + 'Not an archive: clear a resolved request with fleet_task_review_clear. Attached files are kept. '
121
124
  + 'Pass the Markdown you last read as expected_markdown ("" when empty); a concurrent change is refused instead of overwritten.',
122
125
  inputSchema: {
123
126
  markdown: z.string().describe(`Review Markdown (max ${REVIEW_MARKDOWN_MAX} characters). Links use the same safe rules as task descriptions.`),
@@ -127,6 +130,19 @@ export function registerTaskTools(server, binding, admit) {
127
130
  task_id: bound.taskId,
128
131
  review: publicReview(setTaskReviewMarkdown(bound.taskId, { markdown: args.markdown, expectedMarkdown: args.expected_markdown, author })),
129
132
  })));
133
+ server.registerTool('fleet_task_review_clear', {
134
+ description: 'Clear your task\'s resolved To review request after the Owner answered it in the room: removes the Markdown and every attached file in one change. '
135
+ + 'Pass the revision from your last fleet_task_review_show; if the package changed since, nothing is removed and you get a conflict. '
136
+ + 'Record the Owner\'s decision in Progress or the room first. Pass markdown to write the next request in the same fenced change.',
137
+ inputSchema: {
138
+ expected_revision: z.number().int().describe('review.revision as you last read it with fleet_task_review_show.'),
139
+ markdown: z.string().optional().describe(`Optional Markdown of the next request (max ${REVIEW_MARKDOWN_MAX} characters), written in the same change.`),
140
+ },
141
+ }, async (args) => run((bound, author) => {
142
+ const cleared = clearTaskReview(bound.taskId, { expectedRevision: args.expected_revision, author, markdown: args.markdown });
143
+ return { task_id: bound.taskId, review: cleared.review ? publicReview(cleared.review) : null,
144
+ removed: cleared.removed.map(({ request_key: _key, ...artifact }) => artifact) };
145
+ }));
130
146
  server.registerTool('fleet_task_review_detach', {
131
147
  description: 'Remove one file from your task\'s To review package by artifact_id (from fleet_task_review_show).',
132
148
  inputSchema: { artifact_id: z.string() },
@@ -417,6 +417,14 @@ export class FleetResourceViews {
417
417
  // None changes role membership/configuration. Keep these as full wakes.
418
418
  if (structural && parts.length === 2 && ['.session-events.jsonl', '.session-events.jsonl.1', '.subscription-usage.json', '.notify-cursor', '.monitor-state.json', '.monitor-status'].includes(parts[1]))
419
419
  return false;
420
+ // A starting or running member also commits its supervisor/session status
421
+ // (restart ledger, exit, daemon recovery, control endpoint, monitor owner) and
422
+ // runner bookkeeping no projection reads. Neither changes membership; while
423
+ // members start, fencing on them left no stable directory capture.
424
+ if (structural && parts.length === 2 && ['.restart-ledger.json', '.exit-status', '.daemon-recovery.json', '.control-token', '.control.sock', '.monitor-owner', '.booted', '.supervisor-run.json', '.subscription-pin.json', '.session-ownership.json', '.managed-recovery.json', '.acp-session-id', '.self-reminders.json', 'supervisor.log', 'WORKLOG.md'].includes(parts[1]))
425
+ return false;
426
+ if (structural && parts.length >= 2 && ['.ours-bridge', '.task-supervisor'].includes(parts[1]))
427
+ return false;
420
428
  if (parts.length < 3 || parts[1] !== '.conversation')
421
429
  continue;
422
430
  if (structural && (descriptor.resource === 'directory' || descriptor.resource === 'roles'))
@@ -8,18 +8,59 @@ before this feature, or that never use it, have neither and behave as before.
8
8
  browser response also includes `unread` per entry from the same task snapshot,
9
9
  based on persisted entry-ID acknowledgments. Legacy records without read state
10
10
  report all existing entries as read; retry keys remain private. It is
11
- for significant verified milestones (root cause found, PR opened with its link,
12
- a measured result with its honest scope, final deliverable ready), not for room
13
- chat, routine status, handoffs or ordinary review rounds. Entries cannot be
14
- edited or removed.
15
- - **To review** is the self-contained package the Owner reviews before deciding
16
- on the task: Markdown plus retained files. It should let the Owner review
17
- without reading the room or the task description: concise outcome, clickable PR
18
- and specification links, attached documents, validation and required-CI results
11
+ for significant verified facts the Owner should know: delivered behavior, a
12
+ concrete root cause, a measured improvement with a valid comparison, a PR or
13
+ publication milestone, an important blocker, a changed scope or a required
14
+ Owner decision. It is not for room chat, routine status, handoffs or internal
15
+ review rounds. Each entry holds one fact in at most 400 characters (see
16
+ [Limits](#limits-retries-and-concurrency)), leads with the result or the
17
+ required action, names the actor and uses the task's language. Entries cannot
18
+ be edited or removed.
19
+ - **To review** holds one *current* request for human review or decision: the
20
+ final acceptance package, or one specific Owner decision that blocks or steers
21
+ the work. It is not ongoing agent review, an evidence archive or a growing list
22
+ of materials. Markdown plus retained files gather everything needed for that
23
+ decision in one place. The final package lets the Owner review without reading
24
+ the room or the task description: concise outcome, clickable PR and
25
+ specification links, attached documents, validation and required-CI results
19
26
  for the exact heads, the independent verdict where one applies, material limits
20
27
  and any Owner decision. Research and document tasks attach their documents and
21
28
  need no PR.
22
29
 
30
+ ### Progress examples
31
+
32
+ | Instead of | Write |
33
+ | --- | --- |
34
+ | "Started looking into the flaky test, will report back." | Nothing: routine status. |
35
+ | "After a lot of investigation we think the cache might be the cause." | "Developer found the root cause: the cache key omits the workspace ID." |
36
+ | "Critic reviewed PR #42 and asked for two changes, then approved." | "Critic approved PR #42 after the review rounds." (only the outcome) |
37
+ | A 900-character summary of three results | Three entries, one result each. |
38
+ | — | "Owner review required: choose the deployment option in To review." |
39
+
40
+ ### To review lifecycle
41
+
42
+ 1. **Request.** The responsible agent writes the request into To review
43
+ (`fleet_task_review_set`, attachments with the CLI) and appends a Progress
44
+ entry that sends the Owner there, for example "Owner review required: choose
45
+ the deployment option in To review." Final acceptance also uses the Review
46
+ lifecycle; an interim decision never marks unfinished work done.
47
+ 2. **Answer.** Opening To review marks it read but does not resolve it. The
48
+ Owner's answer in the room does.
49
+ 3. **Clear.** The agent that owns the request records the decision in Progress or
50
+ the room, reads the package `revision` with `fleet_task_review_show`, and
51
+ calls `fleet_task_review_clear` with that `expected_revision` (CLI:
52
+ `ours-fleet task review-package TASK --clear --expected-revision N`). The next
53
+ request replaces the cleared one, in the same change when `markdown` (CLI:
54
+ `--markdown-file`) is given; requests do not accumulate.
55
+
56
+ A clear removes the Markdown and every attached file in one change and only when
57
+ the package revision is still the one the writer read. Any Markdown edit, attach
58
+ or detach since then, including an edit back to the same Markdown, makes it a
59
+ conflict and nothing is removed. A file attached after the clear committed is
60
+ kept. Clearing an empty package at its current revision changes nothing. Like
61
+ every package change, a clear gets the next revision: readers see an updated
62
+ package until they view it, and read markers stay valid.
63
+
23
64
  The web App shows both as compact **Progress** and **To review** buttons with
24
65
  counts on the task screen, each opening its own view. Review links use the same
25
66
  safe Markdown and link rules as the task description.
@@ -30,10 +71,10 @@ The packaged default roles divide the work so milestones are not duplicated:
30
71
 
31
72
  | Role | Progress | To review |
32
73
  | --- | --- | --- |
33
- | LocalCoordinator | Default writer: appends significant verified milestones during room turns it already takes. Its 15-minute continuity pass never writes Progress. | Makes sure members prepare the package before a Review handoff; hands it to the Fleet Coordinator. |
34
- | Developer, Engineer | Not by default. | Prepare and keep the package current before reporting completion or asking for Review. |
35
- | Critic | Not by default. | Check that the package matches what was verified; withhold sign-off while it is missing, stale or misleading. |
36
- | Coordinator (Fleet Coordinator) | Not by default. | Before `task review`, check that the package exists and attaches the original description; if not, ask the room to complete it instead of writing it. Then replace the description with only the PR links (or deliverable links when there are no PRs). Finish only when the Owner accepts or asks. |
74
+ | LocalCoordinator | The only writer when present: extracts Owner-relevant facts from room discussion during turns it already takes. Its 15-minute continuity pass never writes Progress. | Makes sure members prepare the package before a Review handoff; hands it to the Fleet Coordinator. Directs the Owner to an interim decision request and clears it after the answer. |
75
+ | Developer, Engineer | Writer only in a room without a LocalCoordinator; otherwise posts such facts in the room. | Prepare and keep the package current before reporting completion or asking for Review; clear a request they wrote once the Owner answered. |
76
+ | Critic | Never; posts findings in the room. | Check that the package matches what was verified; withhold sign-off while it is missing, stale or misleading. Review rounds stay in the room. |
77
+ | Coordinator (Fleet Coordinator) | Not by default. | Before `task review`, check that the package is one current acceptance request and attaches the original description; if not, ask the room to complete it instead of writing it. Then replace the description with only the PR links (or deliverable links when there are no PRs). Finish only when the Owner accepts or asks. |
37
78
 
38
79
  This is default instruction policy, not an access rule: any agent bound to the
39
80
  task, and the local operator, can use the surfaces below. Preparing a package
@@ -49,12 +90,13 @@ to review, including the original task description as an attached file. Package
49
90
  authors save and attach that file before the handoff, the Critic checks it, and
50
91
  the Coordinator replaces the description only once the package has it.
51
92
 
52
- Fresh installations get these instructions with preset revision 12. An existing
93
+ Fresh installations get these instructions with preset revision 13. An existing
53
94
  configuration keeps its installed copies until you run
54
95
  `ours-fleet migrate-role-defaults -c FILE` (a zero-write plan) and then with
55
- `--write`: untouched revision-11 copies of the Developer, Critic, Engineer,
56
- LocalCoordinator and Coordinator roles and the LocalCoordinator Agent Template
57
- adopt the new text, and edited copies are preserved byte-for-byte. Running
96
+ `--write`: untouched revision-11 and revision-12 copies of the Developer, Critic,
97
+ Engineer, LocalCoordinator and Coordinator roles, and the revision-11
98
+ LocalCoordinator Agent Template, adopt the new text, and edited copies are
99
+ preserved byte-for-byte. Running
58
100
  agents keep the instructions they started with until they are restarted.
59
101
 
60
102
  ## Agent tools
@@ -69,7 +111,8 @@ agent that is a listed task member must match that member's identity CID.
69
111
  | `fleet_task_progress_list` | Read the log oldest first, in pages; pass `after` = the previous page's `next_after`. |
70
112
  | `fleet_task_progress_append` | Append one entry (`text`, optional `request_key`). |
71
113
  | `fleet_task_review_show` | Read the package Markdown and file list. |
72
- | `fleet_task_review_set` | Replace the Markdown; pass the Markdown you last read as `expected_markdown` (`""` when empty). |
114
+ | `fleet_task_review_set` | Replace the Markdown; pass the Markdown you last read as `expected_markdown` (`""` when empty). Attached files are kept. |
115
+ | `fleet_task_review_clear` | Clear the resolved request: Markdown and every file, only if `expected_revision` is still current. Optional `markdown` writes the next request in the same change. Returns the package and the removed files. |
73
116
  | `fleet_task_review_detach` | Remove one file by `artifact_id`. |
74
117
 
75
118
  Files are attached with the CLI below. It runs in the agent's own process, so the
@@ -85,6 +128,7 @@ ours-fleet task review-package TASK [--json]
85
128
  ours-fleet task review-package TASK --markdown-file review.md
86
129
  ours-fleet task review-package TASK --attach docs/spec.md [--request-key KEY]
87
130
  ours-fleet task review-package TASK --detach ARTIFACT_ID
131
+ ours-fleet task review-package TASK --clear --expected-revision N [--markdown-file next.md]
88
132
  ```
89
133
 
90
134
  Run inside a managed agent, the supervisor-verified caller is recorded as the
@@ -92,7 +136,11 @@ author (with its identity CID); otherwise the author is the local operator.
92
136
  `--text`, `--text-file`, `--markdown`, `--markdown-file` and `--attach` values are
93
137
  redacted from the supervisor's command ledger. `--markdown-file` replaces the
94
138
  Markdown current at the time of the call; use `fleet_task_review_set` when a
95
- concurrent change must be refused.
139
+ concurrent change must be refused. The text output shows the package revision.
140
+ `--clear` is fenced by `--expected-revision`; with `--markdown` or
141
+ `--markdown-file` the same locked change writes the next request, so no other
142
+ writer can land in between. `--clear` cannot be combined with `--attach`: attach
143
+ the next request's files afterwards.
96
144
 
97
145
  ## HTTP API
98
146
 
@@ -104,7 +152,7 @@ workspace-device bearer. `GET /api/v1/meta` reports API minor version 1.
104
152
  | `GET /tasks`, `GET /tasks/:id` and other task payloads | `progress_count` and `review: {has_markdown, artifact_count, revision, updated_at?}`; `revision` grows with every package change (0 without a package). Bodies are not inlined. |
105
153
  | `GET /tasks/:id/progress` | `{progress: [{id, at, author, text, unread}]}`, oldest first. |
106
154
  | `POST /tasks/:id/progress` | `{text, requestKey?}` (or an `Idempotency-Key` header): 201 new, 200 replay, 409 conflicting replay. |
107
- | `GET /tasks/:id/review` | `{review: {markdown, updated_at, updated_by, artifacts: [{id, name, mime, size, sha256, added_at, added_by}]}}` or `{review: null}`. |
155
+ | `GET /tasks/:id/review` | `{review: {revision, markdown, updated_at, updated_by, artifacts: [{id, name, mime, size, sha256, added_at, added_by}]}}` or `{review: null}`. |
108
156
  | `PUT /tasks/:id/review` | `{markdown, expectedMarkdown}`; 409 when the Markdown changed meanwhile. |
109
157
  | `GET /tasks/:id/review/artifacts/:artifactId` | The retained bytes as `application/octet-stream` attachment with `Content-Security-Policy: default-src 'none'; sandbox`, `nosniff` and `no-store`. Another task's artifact ID is 404. |
110
158
 
@@ -112,8 +160,14 @@ The HTTP API has no upload route: the browser never submits a file path.
112
160
 
113
161
  ## Limits, retries and concurrency
114
162
 
115
- - Progress text: 1-4000 characters, kept exactly as written; whitespace-only text
116
- is rejected. At most 200 entries per task.
163
+ - Progress text: 1-400 characters, kept exactly as written; whitespace-only text
164
+ is rejected. Characters are Unicode code points of the exact submitted text,
165
+ without normalization or trimming: `😀` counts as 1 (although it is two UTF-16
166
+ units), while `é` written as `e` plus a combining accent counts as 2, and a
167
+ joined emoji such as `👩‍💻` counts as 3. Longer text is refused with
168
+ `invalid_request` on every surface (HTTP 400): nothing is saved, cut or split.
169
+ Entries written before this limit keep their text and still display. At most
170
+ 200 entries per task.
117
171
  - Review Markdown: up to 50,000 characters. At most 20 files of 20 MiB each per
118
172
  task, and 200 MiB stored per task including unreferenced copies.
119
173
  - Every change is a read-modify-write under the task's cross-process lock, so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "2.2.3",
3
+ "version": "2.2.5",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -54,8 +54,9 @@ persona: |
54
54
  `ours-fleet task start <task-id>` after addressing the reported blocker.
55
55
 
56
56
  When executors report completion, check that the task's To review package exists and is not empty
57
- with `ours-fleet task review-package <task-id>`: it must let the Owner review without the room or
58
- task description, and that it attaches the original task description. If either is missing, ask the
57
+ with `ours-fleet task review-package <task-id>`: it must be one current acceptance request, not an
58
+ archive of earlier requests, let the Owner review without the room or task description, and
59
+ attach the original task description. If either is missing, ask the
59
60
  room to complete it; do not write or judge it yourself. Then replace the task description with only
60
61
  the clickable Markdown PR links, one per line, with no status or evidence (those stay in To review);
61
62
  a task without PRs links its actual deliverables instead, never an invented PR. Then move the task
@@ -13,7 +13,9 @@ persona: |
13
13
  Withhold sign-off while it is missing, stale or misleading, and record your verdict in it (with
14
14
  `fleet_task_review_set`) or ask its author to. Research and document tasks need their documents,
15
15
  not a PR. Checking the package grants no lifecycle authority: hand the Review move to whoever your
16
- instructions authorize.
16
+ instructions authorize. To review holds only the current request for the Owner, not your ongoing
17
+ review: keep findings and verdict rounds in the room. You do not write Progress; post findings
18
+ the Owner should know in the room for its Progress writer.
17
19
 
18
20
  Task state, progress, checkpoints, findings, review verdicts, completion, handoffs, and ordinary
19
21
  errors are routine task communication: report them only in the assigned Cowork room. When a