@ours.network/fleet 0.19.0-nightly.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +96 -11
  2. package/dist/application/fleet-query-service.d.ts +1 -1
  3. package/dist/application/fleet-query-service.js +12 -0
  4. package/dist/application/role-creation-service.js +3 -1
  5. package/dist/application/types.d.ts +11 -0
  6. package/dist/briefing.d.ts +1 -1
  7. package/dist/briefing.js +103 -10
  8. package/dist/build-info.json +5 -5
  9. package/dist/cli.js +41 -11
  10. package/dist/config.d.ts +21 -7
  11. package/dist/config.js +32 -18
  12. package/dist/creation.d.ts +1 -1
  13. package/dist/docs.d.ts +2 -2
  14. package/dist/docs.js +145 -24
  15. package/dist/doctor.js +10 -10
  16. package/dist/duration.js +1 -1
  17. package/dist/fleet-proxy.d.ts +5 -0
  18. package/dist/harness/acp-agent.js +11 -6
  19. package/dist/harness/claude-code.js +201 -8
  20. package/dist/harness/codex.d.ts +4 -1
  21. package/dist/harness/codex.js +72 -13
  22. package/dist/harness/types.d.ts +57 -6
  23. package/dist/isolation/bubblewrap.d.ts +4 -4
  24. package/dist/isolation/bubblewrap.js +4 -4
  25. package/dist/isolation/policy.d.ts +1 -1
  26. package/dist/isolation/policy.js +2 -2
  27. package/dist/isolation/registry.d.ts +3 -3
  28. package/dist/isolation/registry.js +5 -5
  29. package/dist/isolation/resources.d.ts +1 -1
  30. package/dist/isolation/resources.js +1 -1
  31. package/dist/isolation/types.d.ts +3 -3
  32. package/dist/loops/manager.d.ts +30 -1
  33. package/dist/loops/manager.js +69 -6
  34. package/dist/loops/state.d.ts +18 -0
  35. package/dist/loops/state.js +4 -0
  36. package/dist/model-env.d.ts +71 -0
  37. package/dist/model-env.js +106 -0
  38. package/dist/monitor.d.ts +2 -2
  39. package/dist/monitor.js +4 -4
  40. package/dist/ops.d.ts +2 -2
  41. package/dist/ops.js +9 -9
  42. package/dist/owner-channel/channel.d.ts +17 -0
  43. package/dist/owner-channel/channel.js +104 -23
  44. package/dist/owner-channel/commands.d.ts +9 -0
  45. package/dist/owner-channel/commands.js +226 -99
  46. package/dist/owner-channel/notices.d.ts +7 -0
  47. package/dist/owner-channel/notices.js +9 -0
  48. package/dist/rooms-tasks/cli.js +733 -221
  49. package/dist/rooms-tasks/close.d.ts +35 -0
  50. package/dist/rooms-tasks/close.js +182 -0
  51. package/dist/rooms-tasks/config.js +8 -5
  52. package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
  53. package/dist/rooms-tasks/cowork-adapter.js +116 -0
  54. package/dist/rooms-tasks/external-worker.d.ts +2 -0
  55. package/dist/rooms-tasks/external-worker.js +40 -0
  56. package/dist/rooms-tasks/index.d.ts +2 -0
  57. package/dist/rooms-tasks/index.js +2 -0
  58. package/dist/rooms-tasks/markdown.d.ts +49 -0
  59. package/dist/rooms-tasks/markdown.js +206 -0
  60. package/dist/rooms-tasks/member-startup.d.ts +51 -0
  61. package/dist/rooms-tasks/member-startup.js +151 -0
  62. package/dist/rooms-tasks/provision.d.ts +8 -0
  63. package/dist/rooms-tasks/provision.js +363 -97
  64. package/dist/rooms-tasks/room-state.d.ts +13 -1
  65. package/dist/rooms-tasks/room-state.js +149 -1
  66. package/dist/rooms-tasks/task-state.d.ts +13 -2
  67. package/dist/rooms-tasks/task-state.js +131 -3
  68. package/dist/rooms-tasks/terminal.d.ts +23 -0
  69. package/dist/rooms-tasks/terminal.js +56 -0
  70. package/dist/rooms-tasks/types.d.ts +109 -4
  71. package/dist/rooms-tasks/types.js +1 -1
  72. package/dist/runner.d.ts +48 -0
  73. package/dist/runner.js +252 -100
  74. package/dist/session/acp.d.ts +106 -2
  75. package/dist/session/acp.js +217 -14
  76. package/dist/session/activity.d.ts +31 -0
  77. package/dist/session/activity.js +48 -0
  78. package/dist/session/control.d.ts +1 -1
  79. package/dist/session/conversation-normalizer.d.ts +7 -1
  80. package/dist/session/conversation-normalizer.js +154 -11
  81. package/dist/session/conversation-store.js +1 -1
  82. package/dist/session/conversation-types.d.ts +25 -6
  83. package/dist/session/types.d.ts +35 -0
  84. package/dist/spawn.d.ts +4 -2
  85. package/dist/spawn.js +39 -26
  86. package/dist/supervisor/launchd.d.ts +2 -2
  87. package/dist/supervisor/launchd.js +4 -4
  88. package/dist/supervisor/systemd.js +4 -4
  89. package/dist/supervisor/types.d.ts +1 -1
  90. package/dist/temp-lifecycle.d.ts +14 -2
  91. package/dist/temp-lifecycle.js +90 -0
  92. package/dist/watchdog/alerts.d.ts +2 -2
  93. package/dist/watchdog/alerts.js +2 -2
  94. package/dist/watchdog/briefing.d.ts +5 -5
  95. package/dist/watchdog/briefing.js +13 -6
  96. package/dist/watchdog/config.js +1 -1
  97. package/dist/watchdog/query.d.ts +2 -2
  98. package/dist/watchdog/query.js +2 -2
  99. package/dist/watchdog/report.js +2 -2
  100. package/dist/watchdog/run.d.ts +2 -2
  101. package/dist/watchdog/run.js +6 -6
  102. package/dist/watchdog/scheduler.d.ts +7 -7
  103. package/dist/watchdog/scheduler.js +10 -10
  104. package/dist/watchdog/service.d.ts +4 -4
  105. package/dist/watchdog/service.js +5 -6
  106. package/dist/watchdog/store.d.ts +4 -4
  107. package/dist/watchdog/store.js +4 -4
  108. package/dist/web/runtime.js +1 -1
  109. package/dist/web/server.js +1 -1
  110. package/dist/web/topology-promote.js +1 -1
  111. package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
  112. package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
  113. package/dist/web-app/index.html +1 -1
  114. package/dist/worklog.d.ts +7 -1
  115. package/dist/worklog.js +191 -39
  116. package/package.json +1 -1
@@ -106,7 +106,7 @@ Type=simple
106
106
  ${environmentLines}
107
107
  ExecStart=${unitArg(process.execPath)} ${unitArg(binPath)} _run %i
108
108
  # The RUNNER owns the child-session restart loop, with a counted, backed-off
109
- # circuit breaker (3.2). systemd must only recover the runner PROCESS crashing —
109
+ # circuit breaker. systemd must only recover the runner PROCESS crashing —
110
110
  # Restart=always here would resume the uncounted two-second relaunch loop, and
111
111
  # would also restart a runner that is deliberately holding a failing agent down.
112
112
  Restart=on-failure
@@ -126,12 +126,12 @@ WantedBy=default.target
126
126
  },
127
127
  async install(name) {
128
128
  // Ask FIRST whether this unit was already enabled, so a rollback can tell
129
- // "we registered this" from "it was already here" (6.2).
129
+ // "we registered this" from "it was already here".
130
130
  const before = await ctl('is-enabled', unitFor(name));
131
131
  const alreadyEnabled = before.stdout.trim() === 'enabled';
132
132
  const r = await ctl('enable', '--now', unitFor(name));
133
133
  // Undo only what WE enabled. A unit that was already enabled belongs to
134
- // whoever enabled it, and rollback may never remove that (6.2).
134
+ // whoever enabled it, and rollback may never remove that.
135
135
  const undo = async () => { if (!alreadyEnabled)
136
136
  await ctl('disable', '--now', unitFor(name)); };
137
137
  if (r.code !== 0) {
@@ -156,7 +156,7 @@ WantedBy=default.target
156
156
  // care whether this systemd propagates a start failure into the exit
157
157
  // code, so it is correct both on versions that do and versions that do
158
158
  // not. Only a DEFINITE stop counts. An unanswerable probe is `unknown`
159
- // and must never be read as a failed start (1.1) — the unit may be
159
+ // and must never be read as a failed start — the unit may be
160
160
  // perfectly fine and the bus merely unreachable.
161
161
  const live = await probeLiveness(ctl, name);
162
162
  if (live.state === 'stopped') {
@@ -31,7 +31,7 @@ export interface SupervisorBackend {
31
31
  * Ensure the role's unit exists and is enabled + started. Idempotent, and
32
32
  * EXPLICIT about whether it created the registration: rollback may only
33
33
  * remove what this transaction made, so "did I create this?" has to be
34
- * answerable rather than assumed (6.2).
34
+ * answerable rather than assumed.
35
35
  */
36
36
  install(name: string, binPath: string): Promise<InstallOutcome>;
37
37
  start(name: string): Promise<void>;
@@ -46,11 +46,24 @@ export declare function markTempSupervisorActive(dir: string, pid?: number): Pro
46
46
  export declare function requestedTempStopReason(dir: string): 'operator-stop' | undefined;
47
47
  /** Move retired state out of the live roster without deleting any evidence. */
48
48
  export declare function archiveTempState(role: string, reason: TempTerminationReason, outcome: TempTerminationRecord['outcome'], detail: string, now?: Date): string | undefined;
49
- interface TempLifecycleDeps {
49
+ /** Resolve only an exact role+launch archive; names and timestamps are never proof. */
50
+ export declare function tempArchiveForLaunch(role: string, launchId: string): string | undefined;
51
+ /** Resolve one terminated archive by exact role + durable creation action. */
52
+ export declare function tempArchiveForCreationAction(role: string, creationActionId: string): {
53
+ path: string;
54
+ launchId: string;
55
+ } | undefined;
56
+ /**
57
+ * Preserve a definitively stopped launch as recovery evidence, or prove that
58
+ * its supervisor already did so. Never creates a second archive.
59
+ */
60
+ export declare function secureStoppedTempArchive(role: string, launchId: string, deps?: TempLifecycleDeps): Promise<string>;
61
+ export interface TempLifecycleDeps {
50
62
  exec?: Exec;
51
63
  now?(): number;
52
64
  kill?(pid: number, signal: NodeJS.Signals | 0): void;
53
65
  log?(line: string): void;
66
+ sleep?(ms: number): Promise<void>;
54
67
  }
55
68
  export declare function tempSupervisorLiveness(dir: string, deps?: TempLifecycleDeps): Promise<'running' | 'stopped' | 'unknown'>;
56
69
  export declare function stopTempSupervisor(role: string, deps?: TempLifecycleDeps): Promise<'stopped' | 'already-stopped'>;
@@ -59,4 +72,3 @@ export declare function stopTempSupervisor(role: string, deps?: TempLifecycleDep
59
72
  * Unknown/legacy/live entries are preserved; absence of proof is never cleanup authority.
60
73
  */
61
74
  export declare function reclaimStaleTempState(deps?: TempLifecycleDeps): Promise<string[]>;
62
- export {};
@@ -191,6 +191,96 @@ export function archiveTempState(role, reason, outcome, detail, now = new Date()
191
191
  }
192
192
  return paths.target;
193
193
  }
194
+ function isArchiveForLaunch(path, role, launchId) {
195
+ const supervisor = readTempSupervisor(path);
196
+ if (supervisor?.role !== role || supervisor.launchId !== launchId)
197
+ return false;
198
+ try {
199
+ return readFileSync(join(path, TEMP_TERMINATION_FILE), 'utf8')
200
+ .split('\n')
201
+ .filter(Boolean)
202
+ .some(line => {
203
+ const record = JSON.parse(line);
204
+ return record.version === 1 && record.role === role && record.launchId === launchId;
205
+ });
206
+ }
207
+ catch {
208
+ return false;
209
+ }
210
+ }
211
+ /** Resolve only an exact role+launch archive; names and timestamps are never proof. */
212
+ export function tempArchiveForLaunch(role, launchId) {
213
+ let entries;
214
+ try {
215
+ entries = readdirSync(archiveRoot(), { withFileTypes: true })
216
+ .filter(entry => entry.isDirectory() && !entry.isSymbolicLink() && !entry.name.startsWith('.'))
217
+ .map(entry => join(archiveRoot(), entry.name));
218
+ }
219
+ catch {
220
+ return undefined;
221
+ }
222
+ const matches = entries.filter(path => isArchiveForLaunch(path, role, launchId));
223
+ if (matches.length > 1) {
224
+ throw new Error(`temporary role '${role}' launch ${launchId} has ${matches.length} recovery archives; refusing ambiguity`);
225
+ }
226
+ return matches[0];
227
+ }
228
+ /** Resolve one terminated archive by exact role + durable creation action. */
229
+ export function tempArchiveForCreationAction(role, creationActionId) {
230
+ let entries;
231
+ try {
232
+ entries = readdirSync(archiveRoot(), { withFileTypes: true })
233
+ .filter(entry => entry.isDirectory() && !entry.isSymbolicLink() && !entry.name.startsWith('.'))
234
+ .map(entry => join(archiveRoot(), entry.name));
235
+ }
236
+ catch {
237
+ return undefined;
238
+ }
239
+ const matches = entries.flatMap(path => {
240
+ const supervisor = readTempSupervisor(path);
241
+ if (!supervisor || supervisor.role !== role || !isArchiveForLaunch(path, role, supervisor.launchId))
242
+ return [];
243
+ try {
244
+ const provenance = JSON.parse(readFileSync(join(path, 'creation.json'), 'utf8'));
245
+ return provenance.role === role && provenance.creationActionId === creationActionId
246
+ ? [{ path, launchId: supervisor.launchId }] : [];
247
+ }
248
+ catch {
249
+ return [];
250
+ }
251
+ });
252
+ if (matches.length > 1) {
253
+ throw new Error(`temporary role '${role}' action ${creationActionId} has ${matches.length} recovery archives; refusing ambiguity`);
254
+ }
255
+ return matches[0];
256
+ }
257
+ /**
258
+ * Preserve a definitively stopped launch as recovery evidence, or prove that
259
+ * its supervisor already did so. Never creates a second archive.
260
+ */
261
+ export async function secureStoppedTempArchive(role, launchId, deps = {}) {
262
+ const dir = join(tmpRoot(), role);
263
+ if (existsSync(dir)) {
264
+ const supervisor = readTempSupervisor(dir);
265
+ if (!supervisor || supervisor.role !== role || supervisor.launchId !== launchId) {
266
+ throw new Error(`temporary role '${role}' no longer matches recorded launch ${launchId}; refusing to archive`);
267
+ }
268
+ const live = await tempSupervisorLiveness(dir, deps);
269
+ if (live !== 'stopped') {
270
+ throw new Error(`temporary role '${role}' supervisor is ${live}; refusing to archive before proven stop`);
271
+ }
272
+ const archived = archiveTempState(role, 'operator-stop', 'retired', 'room close proved the supervisor stopped; full role evidence preserved');
273
+ if (!archived || !isArchiveForLaunch(archived, role, launchId)) {
274
+ throw new Error(`temporary role '${role}' archive could not be verified for launch ${launchId}`);
275
+ }
276
+ return archived;
277
+ }
278
+ const archived = tempArchiveForLaunch(role, launchId);
279
+ if (!archived) {
280
+ throw new Error(`temporary role '${role}' live state disappeared without an exact recovery archive for launch ${launchId}`);
281
+ }
282
+ return archived;
283
+ }
194
284
  /** Finish bounded archive renames interrupted by process or host termination. */
195
285
  function recoverInterruptedArchives(now) {
196
286
  let names;
@@ -21,7 +21,7 @@ export interface AlertLedger {
21
21
  export declare function readLedger(name: string): AlertLedger;
22
22
  export declare function writeLedger(name: string, l: AlertLedger): void;
23
23
  /**
24
- * Reconcile rules (spec §5): an `error`-status report carries no role evidence, so the ledger
24
+ * An `error`-status report carries no role evidence, so the ledger
25
25
  * is returned unchanged. For each finding with rank > 0: an existing open entry keeps its
26
26
  * `since` and updates `status` on escalation/de-escalation; a new one opens with `since = now`.
27
27
  * Any role reported `healthy`/`idle` closes (deletes) its open entry. Every role named in
@@ -30,5 +30,5 @@ export declare function writeLedger(name: string, l: AlertLedger): void;
30
30
  * Never mutates `l` — always returns a new ledger object.
31
31
  */
32
32
  export declare function reconcileLedger(l: AlertLedger, report: WatchdogReport, now: Date): AlertLedger;
33
- /** Ledger's open findings + cooldown, in the shape watch.json's `digest` field carries (Task 7 writes it, run reads it back). */
33
+ /** Ledger open findings and cooldown in the shape carried by watch.json's `digest` field. */
34
34
  export declare function computeDigest(l: AlertLedger, cooldownMs: number, now: Date): WatchManifest['digest'];
@@ -30,7 +30,7 @@ export function writeLedger(name, l) {
30
30
  chmodSync(path, 0o600);
31
31
  }
32
32
  /**
33
- * Reconcile rules (spec §5): an `error`-status report carries no role evidence, so the ledger
33
+ * An `error`-status report carries no role evidence, so the ledger
34
34
  * is returned unchanged. For each finding with rank > 0: an existing open entry keeps its
35
35
  * `since` and updates `status` on escalation/de-escalation; a new one opens with `since = now`.
36
36
  * Any role reported `healthy`/`idle` closes (deletes) its open entry. Every role named in
@@ -64,7 +64,7 @@ export function reconcileLedger(l, report, now) {
64
64
  }
65
65
  return { ...l, open };
66
66
  }
67
- /** Ledger's open findings + cooldown, in the shape watch.json's `digest` field carries (Task 7 writes it, run reads it back). */
67
+ /** Ledger open findings and cooldown in the shape carried by watch.json's `digest` field. */
68
68
  export function computeDigest(l, cooldownMs, now) {
69
69
  return {
70
70
  cooldown_ms: cooldownMs,
@@ -6,8 +6,8 @@ export interface WatchManifestRole {
6
6
  stateDir: string;
7
7
  }
8
8
  /**
9
- * The fixed run manifest a watchdog run reads at `manifestPath` (7.3 §3, Task 7 writes it,
10
- * Task 12+ digest reads it back). It carries everything the run needs to identify itself in
9
+ * The fixed run manifest a watchdog run reads at `manifestPath`. It carries everything the
10
+ * run and digest reader need to identify the run in
11
11
  * report.json (`watchdog`, `run_id`, `started_at`) plus the suppression digest that makes
12
12
  * alerting idempotent across runs.
13
13
  */
@@ -34,7 +34,7 @@ export interface WatchdogBriefingOpts {
34
34
  vocabulary: BriefingVocab;
35
35
  /** What spawn actually established about the watchdog's ours identity (7.3, decision 3). */
36
36
  identityGuarantee: 'verified' | 'created' | 'unverified';
37
- /** Raw prompt_file content, appended as extra focus — never a replacement (owner decision 1). */
37
+ /** Raw prompt_file content, appended as extra focus — never a replacement. */
38
38
  promptFocus?: string;
39
39
  }
40
40
  /**
@@ -42,8 +42,8 @@ export interface WatchdogBriefingOpts {
42
42
  * run): bind, observe-only rules, procedure, status vocabulary, evidence rules, alert rules,
43
43
  * report schema, and — if the watchdog configures one — an appended prompt_file focus.
44
44
  *
45
- * The contract is fixed for every watchdog (spec §5: "The schema is fixed for every watchdog,
46
- * including ones with a prompt_file; the override adds focus, never fields"), so every section
45
+ * The contract is fixed for every watchdog, including ones with a prompt_file; the override
46
+ * adds focus, never fields. Therefore every section
47
47
  * below is unconditional except the identity-guarantee wording and the trailing focus append.
48
48
  */
49
49
  export declare function generateWatchdogBriefing(opts: WatchdogBriefingOpts): string;
@@ -27,8 +27,8 @@ function bindSection(heading, wd, v, guarantee) {
27
27
  * run): bind, observe-only rules, procedure, status vocabulary, evidence rules, alert rules,
28
28
  * report schema, and — if the watchdog configures one — an appended prompt_file focus.
29
29
  *
30
- * The contract is fixed for every watchdog (spec §5: "The schema is fixed for every watchdog,
31
- * including ones with a prompt_file; the override adds focus, never fields"), so every section
30
+ * The contract is fixed for every watchdog, including ones with a prompt_file; the override
31
+ * adds focus, never fields. Therefore every section
32
32
  * below is unconditional except the identity-guarantee wording and the trailing focus append.
33
33
  */
34
34
  export function generateWatchdogBriefing(opts) {
@@ -41,7 +41,7 @@ export function generateWatchdogBriefing(opts) {
41
41
  L.push('no memory from any previous run — this run stands alone, and nothing you learn here');
42
42
  L.push('persists past writing your report.');
43
43
  L.push(...bindSection('## 1. Bind your identity', wd, v, guarantee));
44
- // 2. Observe-only rules, verbatim (spec §1).
44
+ // 2. Observe-only rules.
45
45
  L.push('', '## 2. Observe-only — non-negotiable');
46
46
  L.push('You observe and report. You never restart, stop, spawn or remove a role, never answer a ' +
47
47
  'pending permission, never edit a workspace or fleet.yaml, and never approve anything on the ' +
@@ -73,13 +73,20 @@ export function generateWatchdogBriefing(opts) {
73
73
  L.push('3. If a command fails, times out, or the control plane is otherwise unanswerable for a');
74
74
  L.push(' role, that role degrades to `unknown` — never guess `healthy`. `unknown` means the');
75
75
  L.push(' evidence is insufficient, not that the role is fine.');
76
- // 4. Status vocabulary spec §4 definitions, verbatim where quoted.
76
+ // 4. Status vocabulary and definitions.
77
77
  L.push('', '## 4. Status vocabulary (fixed)');
78
78
  L.push(`interval = ${formatDuration(wd.intervalMs)} — thresholds below are multiples of this.`);
79
79
  L.push('');
80
80
  L.push('- `healthy` — alive, on-briefing, recent progress.');
81
81
  L.push('- `idle` — alive, nothing assigned or nothing to do. Not an anomaly.');
82
82
  L.push('- `stale` = no worklog append and no console progress for ≥ 3 intervals.');
83
+ L.push('');
84
+ L.push('`session.readiness` from `ours-fleet status` is TURN OCCUPANCY, not activity: a mail');
85
+ L.push('wake delivered by ACP steering runs an entire turn while readiness stays `idle`. Never');
86
+ L.push('report `idle` or `stale` from `readiness=idle` alone — corroborate with the');
87
+ L.push('`activity:` line of the same `status` output (`active` means the agent is working),');
88
+ L.push('the worklog, or `ours-fleet peek`. `activity: unobservable` is missing evidence, not');
89
+ L.push('an idle agent.');
83
90
  L.push('- `blocked` = waiting on a permission/prompt/modal longer than one interval.');
84
91
  L.push('- `off_briefing` — activity contradicts the briefing (wrong repo, out-of-scope work,');
85
92
  L.push(' ignored routine).');
@@ -92,7 +99,7 @@ export function generateWatchdogBriefing(opts) {
92
99
  L.push('items (`{source, detail, observed_at}`), each ≤280 characters. Never include env');
93
100
  L.push('values, tokens, file contents beyond quoted evidence, or a full pane dump — quote only');
94
101
  L.push('the specific line(s) that support the finding.');
95
- // 6. Alert rules — spec §5 rules 1-4, plus the digest-authoritative sentence, verbatim.
102
+ // 6. Alert rules plus the digest-authoritative sentence.
96
103
  L.push('', '## 6. Alert rules');
97
104
  L.push('1. Alert on a finding that is **new** (role+status not open) or **escalated** (e.g.');
98
105
  L.push(' `stale` → `blocked`).');
@@ -109,7 +116,7 @@ export function generateWatchdogBriefing(opts) {
109
116
  'realert_after; send one resolved notice when a digest-open role is now healthy; ONE message ' +
110
117
  `per run listing all qualifying findings, sent with **${v.sendTool}** to "${wd.coordinator}"; ` +
111
118
  'set alerted:true and add an alerts[] entry for each. If nothing qualifies, send nothing.');
112
- // 7. Report schema — spec §5 example, plus the write-order sentence, verbatim.
119
+ // 7. Report schema plus the write-order sentence.
113
120
  L.push('', '## 7. Write your report');
114
121
  L.push('`report.json` has this fixed schema (`schema_version` is always the literal `1`;');
115
122
  L.push('`status` is `ok` when every role is `healthy`/`idle`, `anomalies` when any role is not,');
@@ -29,7 +29,7 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
29
29
  throw new ConfigError(`${baseFile}: watchdogs: must be a map`);
30
30
  const roleNames = new Set(roles.map(r => r.name));
31
31
  const roleIdentities = new Set(roles.map(r => r.identity));
32
- // Tracks identity -> owning watchdog name across entries (final review #5):
32
+ // Tracks identity -> owning watchdog name across entries:
33
33
  // two watchdogs declaring the same identity would otherwise share a temp
34
34
  // dir, tmux session, and run lock with no error until they collide at
35
35
  // runtime.
@@ -7,7 +7,7 @@ export interface WatchdogRoleFinding {
7
7
  reason: string;
8
8
  }
9
9
  /**
10
- * Needs-attention integration (Task 19): worst current finding per role across
10
+ * Needs-attention integration: worst current finding per role across
11
11
  * every configured watchdog, for FleetQueryService.status() to fold into a
12
12
  * role's problems. An `error`-status report carries no role evidence (the run
13
13
  * itself failed) so it's skipped outright, matching alerts.ts's
@@ -17,7 +17,7 @@ export interface WatchdogRoleFinding {
17
17
  * parameter (rather than importing store.ts directly) so it stays a pure,
18
18
  * disk-free function for unit testing; runtime.ts wires the real store.
19
19
  *
20
- * Skips disabled watchdogs (finding #5): a watchdog turned off in config
20
+ * Skips disabled watchdogs: a watchdog turned off in config
21
21
  * still has its last stored report sitting on disk, and without this check
22
22
  * that stale report's findings would pin roles in "Needs attention" forever
23
23
  * — an operator disabling a noisy/broken watchdog has no way to make the
@@ -7,7 +7,7 @@ import { WATCHDOG_STATUS_RANK } from './alerts.js';
7
7
  import { readSchedulerState } from './scheduler.js';
8
8
  import { listRuns, readReport } from './store.js';
9
9
  /**
10
- * Needs-attention integration (Task 19): worst current finding per role across
10
+ * Needs-attention integration: worst current finding per role across
11
11
  * every configured watchdog, for FleetQueryService.status() to fold into a
12
12
  * role's problems. An `error`-status report carries no role evidence (the run
13
13
  * itself failed) so it's skipped outright, matching alerts.ts's
@@ -17,7 +17,7 @@ import { listRuns, readReport } from './store.js';
17
17
  * parameter (rather than importing store.ts directly) so it stays a pure,
18
18
  * disk-free function for unit testing; runtime.ts wires the real store.
19
19
  *
20
- * Skips disabled watchdogs (finding #5): a watchdog turned off in config
20
+ * Skips disabled watchdogs: a watchdog turned off in config
21
21
  * still has its last stored report sitting on disk, and without this check
22
22
  * that stale report's findings would pin roles in "Needs attention" forever
23
23
  * — an operator disabling a noisy/broken watchdog has no way to make the
@@ -84,7 +84,7 @@ export function normalizeWatchdogReport(r, ctx) {
84
84
  copy.watchdog = ctx.watchdog;
85
85
  copy.run_id = ctx.run_id;
86
86
  copy.roles = copy.roles.map(role => {
87
- // Rebuilt from ONLY the known fields (final review #6a) — the old
87
+ // Rebuilt from ONLY the known fields — the old
88
88
  // `{ ...role }` spread let any unknown per-finding key (e.g. an agent
89
89
  // dumping a huge `pane_dump`) ride unbounded into the store. Report-level
90
90
  // extras (tail/isolation) are untouched here and stay tolerated; this is
@@ -119,7 +119,7 @@ export function errorReport(ctx) {
119
119
  // Scheduler-built error strings can embed raw agent-controlled bytes
120
120
  // (JSON.parse messages, validator interpolations of agent-supplied
121
121
  // values) and are stored+printed as-is elsewhere — clean them the same
122
- // way finding-level text is cleaned (final review #6b).
122
+ // way finding-level text is cleaned.
123
123
  error: cleanEvidence(ctx.error),
124
124
  ...(ctx.tail !== undefined ? { tail: ctx.tail.slice(-4096) } : {}),
125
125
  };
@@ -36,7 +36,7 @@ export interface WatchdogRunOutcome {
36
36
  * didn't), store the result, and always clean up the temp dir.
37
37
  *
38
38
  * The scheduler's run-lock guarantees only one run per watchdog at a time;
39
- * this function itself takes no lock (Task 8).
39
+ * this function itself takes no lock.
40
40
  */
41
41
  export declare function executeWatchdogRun(wd: ResolvedWatchdog, deps: WatchdogRunDeps): Promise<WatchdogRunOutcome>;
42
42
  /**
@@ -46,7 +46,7 @@ export declare function executeWatchdogRun(wd: ResolvedWatchdog, deps: WatchdogR
46
46
  * (owner-approved deviation 4), so this is how a scheduler tick that can't reach an operator any
47
47
  * other way still gets a message out.
48
48
  *
49
- * Reuses the Task 7/8 run machinery (temp-dir prep, ensureIdentity, ResolvedRole shape, child
49
+ * Reuses the ordinary run machinery (temp-dir prep, ensureIdentity, ResolvedRole shape, child
50
50
  * launch, kill mechanics) but stores no report, touches no ledger findings, and writes no
51
51
  * watch.json manifest — `sent.json` is this run's only completion sentinel. A fixed 2-minute
52
52
  * deadline applies regardless of `wd.timeoutMs`.
@@ -23,7 +23,7 @@ const POLL_MS = 1000;
23
23
  const WRITE_STABLE_MS = 2000;
24
24
  /** Grace given to the agent after a stable report before the session is killed. */
25
25
  const HARVEST_GRACE_MS = 5000;
26
- /** Fixed deadline for a one-shot notifier run — always 2 minutes, never wd.timeoutMs (Task 14). */
26
+ /** Fixed deadline for a one-shot notifier run — always 2 minutes, never wd.timeoutMs. */
27
27
  const NOTIFIER_TIMEOUT_MS = 120_000;
28
28
  /**
29
29
  * Poll for a completion sentinel file (report.json for an inspection run, sent.json for a
@@ -145,7 +145,7 @@ async function discoverLiveTemporaryRoles() {
145
145
  * didn't), store the result, and always clean up the temp dir.
146
146
  *
147
147
  * The scheduler's run-lock guarantees only one run per watchdog at a time;
148
- * this function itself takes no lock (Task 8).
148
+ * this function itself takes no lock.
149
149
  */
150
150
  export async function executeWatchdogRun(wd, deps) {
151
151
  const now = deps.now ?? (() => new Date());
@@ -196,7 +196,7 @@ export async function executeWatchdogRun(wd, deps) {
196
196
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
197
197
  // Snapshot the fleet start-stagger so the detached run (no config path
198
198
  // threaded through `_run-watchdog`) honors the same host-wide launch gate
199
- // as every other role (spec §3) — mirrors src/spawn.ts:503-504.
199
+ // as every other role — mirrors the spawn path.
200
200
  if (cfg.startStaggerMs > 0)
201
201
  writeFileSync(join(dir, START_STAGGER_FILE), String(cfg.startStaggerMs));
202
202
  const launch = deps.launchChild ?? defaultLaunchChild;
@@ -234,7 +234,7 @@ export async function executeWatchdogRun(wd, deps) {
234
234
  report.finished_at = now().toISOString();
235
235
  }
236
236
  }
237
- // Isolation degradation (spec §7): a weaker guarantee must never look like
237
+ // A weaker isolation guarantee must never look like
238
238
  // the strong one, so stamp it on every outcome, error reports included.
239
239
  if (existsSync(join(dir, '.isolation-degraded')))
240
240
  report.isolation = 'degraded';
@@ -257,7 +257,7 @@ export async function executeWatchdogRun(wd, deps) {
257
257
  * (owner-approved deviation 4), so this is how a scheduler tick that can't reach an operator any
258
258
  * other way still gets a message out.
259
259
  *
260
- * Reuses the Task 7/8 run machinery (temp-dir prep, ensureIdentity, ResolvedRole shape, child
260
+ * Reuses the ordinary run machinery (temp-dir prep, ensureIdentity, ResolvedRole shape, child
261
261
  * launch, kill mechanics) but stores no report, touches no ledger findings, and writes no
262
262
  * watch.json manifest — `sent.json` is this run's only completion sentinel. A fixed 2-minute
263
263
  * deadline applies regardless of `wd.timeoutMs`.
@@ -308,7 +308,7 @@ export async function executeNotifierRun(wd, text, deps) {
308
308
  }));
309
309
  // loadTempRole (runner.ts) needs this to run the child via `_run-watchdog`.
310
310
  writeFileSync(join(dir, 'role.yaml'), stringify(role));
311
- // Same host-wide launch-gate snapshot executeWatchdogRun writes (spec §3).
311
+ // Use the same host-wide launch-gate snapshot executeWatchdogRun writes.
312
312
  if (cfg.startStaggerMs > 0)
313
313
  writeFileSync(join(dir, START_STAGGER_FILE), String(cfg.startStaggerMs));
314
314
  const launch = deps.launchChild ?? defaultLaunchChild;
@@ -15,14 +15,14 @@ export declare function readSchedulerState(name: string): WatchdogSchedulerState
15
15
  /** Write never throws: scheduler diagnostics must never take the loop down. */
16
16
  export declare function writeSchedulerState(name: string, s: WatchdogSchedulerState): void;
17
17
  /**
18
- * Operator release (Task 15): clears failures and heldDown so a held-down
18
+ * Operator release clears failures and heldDown so a held-down
19
19
  * loop's next held-down poll sees a clean state and resumes running. Also
20
20
  * clears the ledger's `heldDownAlerted` flag — that flag is what makes "alert
21
21
  * once per hold-down" durable across scheduler restarts (it lives in
22
22
  * alerts.json, not state.json), so a release must reset it too or a
23
23
  * subsequent hold-down would silently alert zero times.
24
24
  *
25
- * Also reclaims a STALE run lock (final review #2, tightened by finding #2):
25
+ * Also reclaims a demonstrably stale run lock:
26
26
  * a watchdog SIGKILLed mid-run (e.g. systemd's TimeoutStopSec on a
27
27
  * fleet-wide stop) leaves `.run-lock` behind forever — the loop's `finally`
28
28
  * that would normally release it never runs. Without this, every future tick
@@ -40,7 +40,7 @@ export declare function writeSchedulerState(name: string, s: WatchdogSchedulerSt
40
40
  export declare function resetSchedulerState(name: string): void;
41
41
  export declare const WATCHDOG_HOLD_THRESHOLD = 3;
42
42
  export declare const WATCHDOG_BACKOFF_MAX_MS = 3600000;
43
- /** Bounded exponential backoff: 1x, 2x, 4x, ... capped at WATCHDOG_BACKOFF_MAX_MS (spec §3). */
43
+ /** Bounded exponential backoff: 1x, 2x, 4x, ... capped at WATCHDOG_BACKOFF_MAX_MS. */
44
44
  export declare function watchdogBackoffMs(intervalMs: number, failures: number): number;
45
45
  export interface SchedulerDeps {
46
46
  now(): Date;
@@ -49,7 +49,7 @@ export interface SchedulerDeps {
49
49
  binPath: string;
50
50
  /**
51
51
  * The fleet config `runScheduler` loaded from the `-c FILE`/default path
52
- * (final review #1). Threaded into both `runOnceFor`'s and the notifier's
52
+ * Threaded into both `runOnceFor`'s and the notifier's
53
53
  * deps so a run under a non-default config doesn't silently fall back to
54
54
  * `loadConfig()`'s default `~/fleet.yaml`. `runWatchdogLoop` callers that
55
55
  * bypass `runScheduler` (tests) may omit it — the run/notifier machinery
@@ -61,7 +61,7 @@ export interface SchedulerDeps {
61
61
  /** Loop exit for tests + SIGTERM (wired by the CLI, not here). */
62
62
  shouldStop?(): boolean;
63
63
  /**
64
- * Scheduler-level alert hook (Task 14, spec §5.5): fired once per hold-down
64
+ * Scheduler-level alert hook: fired once per hold-down
65
65
  * transition (guarded in `settle` by `ledger.heldDownAlerted`, cleared by
66
66
  * `resetSchedulerState`). Default: `executeNotifierRun` — the fleet
67
67
  * process can't message on its own (deviation 4), so the default delivers
@@ -82,7 +82,7 @@ export interface SchedulerDeps {
82
82
  * Injectable run-lock primitives. Default: store's acquireRunLock /
83
83
  * releaseRunLock. Both are mkdir/rmdir-backed and can throw (EACCES, EIO,
84
84
  * ENOTEMPTY on release) — the loop always classifies such a throw as a
85
- * failed tick rather than letting it escape (review finding #1).
85
+ * failed tick rather than letting it escape.
86
86
  */
87
87
  locks?: {
88
88
  acquire(name: string): boolean;
@@ -99,7 +99,7 @@ export interface SchedulerDeps {
99
99
  export declare function runWatchdogLoop(wd: ResolvedWatchdog, deps: SchedulerDeps): Promise<void>;
100
100
  /**
101
101
  * Run every enabled watchdog's loop concurrently until deps.shouldStop().
102
- * SIGTERM wiring into shouldStop is the CLI's job (Task 10), not this
102
+ * SIGTERM wiring into shouldStop is the CLI's job, not this
103
103
  * function's.
104
104
  */
105
105
  export declare function runScheduler(configPath: string | undefined, deps: SchedulerDeps): Promise<void>;
@@ -27,14 +27,14 @@ export function writeSchedulerState(name, s) {
27
27
  catch { /* diagnostics must never take the loop down */ }
28
28
  }
29
29
  /**
30
- * Operator release (Task 15): clears failures and heldDown so a held-down
30
+ * Operator release clears failures and heldDown so a held-down
31
31
  * loop's next held-down poll sees a clean state and resumes running. Also
32
32
  * clears the ledger's `heldDownAlerted` flag — that flag is what makes "alert
33
33
  * once per hold-down" durable across scheduler restarts (it lives in
34
34
  * alerts.json, not state.json), so a release must reset it too or a
35
35
  * subsequent hold-down would silently alert zero times.
36
36
  *
37
- * Also reclaims a STALE run lock (final review #2, tightened by finding #2):
37
+ * Also reclaims a demonstrably stale run lock:
38
38
  * a watchdog SIGKILLed mid-run (e.g. systemd's TimeoutStopSec on a
39
39
  * fleet-wide stop) leaves `.run-lock` behind forever — the loop's `finally`
40
40
  * that would normally release it never runs. Without this, every future tick
@@ -61,7 +61,7 @@ export function resetSchedulerState(name) {
61
61
  }
62
62
  export const WATCHDOG_HOLD_THRESHOLD = 3;
63
63
  export const WATCHDOG_BACKOFF_MAX_MS = 3_600_000;
64
- /** Bounded exponential backoff: 1x, 2x, 4x, ... capped at WATCHDOG_BACKOFF_MAX_MS (spec §3). */
64
+ /** Bounded exponential backoff: 1x, 2x, 4x, ... capped at WATCHDOG_BACKOFF_MAX_MS. */
65
65
  export function watchdogBackoffMs(intervalMs, failures) {
66
66
  if (failures <= 0)
67
67
  return intervalMs;
@@ -91,7 +91,7 @@ export async function runWatchdogLoop(wd, deps) {
91
91
  * before the next tick. On the tick that transitions into hold-down, sleep
92
92
  * `heldPollMs` rather than the (possibly hour-long) backoff delay — an
93
93
  * operator's resetSchedulerState must be noticed within one poll cycle,
94
- * not after the last backoff finishes (review finding #2).
94
+ * not after the last backoff finishes.
95
95
  */
96
96
  const settle = async (isFailure, errorMessage, startedAt) => {
97
97
  const s = readSchedulerState(wd.name);
@@ -116,7 +116,7 @@ export async function runWatchdogLoop(wd, deps) {
116
116
  if (justHeld) {
117
117
  const msg = `watchdog ${wd.name} held down after ${WATCHDOG_HOLD_THRESHOLD} consecutive failed runs: ${s.lastError}`;
118
118
  deps.log(msg);
119
- // "Once per state change" (spec §5.5) must survive a scheduler restart, so the guard
119
+ // "Once per state change" must survive a scheduler restart, so the guard
120
120
  // lives in the ledger (alerts.json), not in memory — resetSchedulerState clears it.
121
121
  const ledger = readLedger(wd.name);
122
122
  if (!ledger.heldDownAlerted) {
@@ -141,7 +141,7 @@ export async function runWatchdogLoop(wd, deps) {
141
141
  // rethrows non-ENOENT failures). Never let that throw escape this loop:
142
142
  // runScheduler drives every watchdog's loop via Promise.all, so an
143
143
  // uncaught throw here would kill every OTHER watchdog's loop too.
144
- // Classify it as this tick's failure instead (review finding #1).
144
+ // Classify it as this tick's failure instead.
145
145
  let acquired = false;
146
146
  let acquireError;
147
147
  try {
@@ -166,7 +166,7 @@ export async function runWatchdogLoop(wd, deps) {
166
166
  // Failure count is untouched by a skip; only lastRunAt/nextRunAt move.
167
167
  // The backoff cadence (not the raw interval) still governs during an
168
168
  // active failure streak: a skip isn't a finished run, so it shouldn't
169
- // reset the retry cadence to full speed either (spec §3).
169
+ // reset the retry cadence to full speed either.
170
170
  const skipState = readSchedulerState(wd.name);
171
171
  const delay = watchdogBackoffMs(wd.intervalMs, skipState.consecutiveFailures);
172
172
  writeSchedulerState(wd.name, {
@@ -216,14 +216,14 @@ export async function runWatchdogLoop(wd, deps) {
216
216
  }
217
217
  /**
218
218
  * Run every enabled watchdog's loop concurrently until deps.shouldStop().
219
- * SIGTERM wiring into shouldStop is the CLI's job (Task 10), not this
219
+ * SIGTERM wiring into shouldStop is the CLI's job, not this
220
220
  * function's.
221
221
  */
222
222
  export async function runScheduler(configPath, deps) {
223
223
  const cfg = loadConfig(configPath);
224
224
  const watchdogs = cfg.watchdogs.filter(w => w.enabled);
225
- // Recover from a SIGKILLed prior run (final review #2a, tightened by
226
- // finding #2): a scheduler restart is EVIDENCE, not proof, that no
225
+ // Recover from a SIGKILLed prior run: a scheduler restart is EVIDENCE,
226
+ // not proof, that no
227
227
  // scheduler-owned run is in flight — a foreground `watchdog-run`, or
228
228
  // another scheduler instance, may genuinely still hold a watchdog's lock.
229
229
  // Only a DEMONSTRABLY stale lock (dead owner pid, or a legacy lock with no
@@ -3,7 +3,7 @@ export declare const WATCHDOG_SYSTEMD_UNIT = "ours-fleet-watchdogs.service";
3
3
  export declare const WATCHDOG_LAUNCHD_LABEL = "network.ours.fleet.watchdogs";
4
4
  /**
5
5
  * Supervises the single long-running watchdog-scheduler process (the hidden
6
- * `_run-watchdogs` command, Task 9's `runScheduler`) the same way
6
+ * `_run-watchdogs` command and `runScheduler`) the same way
7
7
  * `WebServiceManager` (src/web/service.ts) supervises the web console: a
8
8
  * private systemd --user unit on Linux, a launchd LaunchAgent on macOS.
9
9
  */
@@ -16,7 +16,7 @@ export declare class WatchdogServiceManager {
16
16
  supervised(): boolean;
17
17
  /**
18
18
  * Writes the unit/plist and returns whether its content actually changed
19
- * (finding #4): `binPath`/`configPath` are the only inputs that ever
19
+ * `binPath`/`configPath` are the only inputs that ever
20
20
  * change this content, and neither reflects a watchdog's `interval:` or
21
21
  * any other config value inside `watchdogs:` — so `changed` here can never
22
22
  * by itself justify a restart on every config edit. It's one of the two
@@ -33,8 +33,8 @@ export declare class WatchdogServiceManager {
33
33
  * a running service just returns 0 without reloading anything, and
34
34
  * launchctl kickstart (without -k) behaves the same way. That means a
35
35
  * config change (new/changed watchdogs) never reaches a live scheduler
36
- * process via reconcileWatchdogScheduler's `install` + `start` pair (final
37
- * review #4). `restart()` mirrors WebServiceManager.restart: an
36
+ * process via reconcileWatchdogScheduler's `install` + `start` pair.
37
+ * `restart()` mirrors WebServiceManager.restart: an
38
38
  * unconditional restart on Linux, and `kickstart -k` (force-restart) on
39
39
  * macOS, falling back to stop+start if the kickstart itself fails.
40
40
  */