@integrity-labs/agt-cli 0.28.772 → 0.28.774

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.
@@ -18,7 +18,7 @@ import {
18
18
  rotateDailySession,
19
19
  sessionFileExists,
20
20
  todayLocalIso
21
- } from "./chunk-UPSDSMNC.js";
21
+ } from "./chunk-PMKI7ZTP.js";
22
22
  import {
23
23
  reapOrphanChannelMcps
24
24
  } from "./chunk-XWVM4KPK.js";
@@ -5141,4 +5141,4 @@ export {
5141
5141
  stopAllSessionsAndWait,
5142
5142
  getProjectDir
5143
5143
  };
5144
- //# sourceMappingURL=chunk-R6XGVTQS.js.map
5144
+ //# sourceMappingURL=chunk-SXTHAF3R.js.map
@@ -100,7 +100,7 @@ async function spawnPairSession(session) {
100
100
  return { ok: true };
101
101
  } catch {
102
102
  }
103
- const { resolveClaudeBinary } = await import("./persistent-session-NUVUR3PB.js");
103
+ const { resolveClaudeBinary } = await import("./persistent-session-C2Y4CSAY.js");
104
104
  const claudeBin = resolveClaudeBinary();
105
105
  const pairEnv = {
106
106
  ...process.env,
@@ -373,4 +373,4 @@ export {
373
373
  startClaudePair,
374
374
  submitClaudePairCode
375
375
  };
376
- //# sourceMappingURL=claude-pair-runtime-Z7JFVKIY.js.map
376
+ //# sourceMappingURL=claude-pair-runtime-O7T3PIMT.js.map
@@ -58,7 +58,7 @@ import {
58
58
  safeWriteJsonAtomic,
59
59
  setConfigHash,
60
60
  tripClass
61
- } from "../chunk-SBAX57SN.js";
61
+ } from "../chunk-BUCKLMKD.js";
62
62
  import {
63
63
  getProjectDir as getProjectDir2,
64
64
  getReadyTasks,
@@ -120,7 +120,7 @@ import {
120
120
  takeZombieDetection,
121
121
  toOpencodeModel,
122
122
  writeEgressAllowlist
123
- } from "../chunk-R6XGVTQS.js";
123
+ } from "../chunk-SXTHAF3R.js";
124
124
  import {
125
125
  ACCOUNT_ENFORCEMENT_MARKER_FILENAME,
126
126
  AnchorSessionClient,
@@ -209,7 +209,7 @@ import {
209
209
  subagentActivityAgeSeconds,
210
210
  sumTranscriptUsageInWindow,
211
211
  transcriptActivityAgeSeconds
212
- } from "../chunk-UPSDSMNC.js";
212
+ } from "../chunk-PMKI7ZTP.js";
213
213
  import {
214
214
  reapOrphanChannelMcps
215
215
  } from "../chunk-XWVM4KPK.js";
@@ -12870,7 +12870,7 @@ var pendingDayRolloverReset = /* @__PURE__ */ new Set();
12870
12870
  var dayRolloverInboundHold = /* @__PURE__ */ new Map();
12871
12871
  var INBOUND_HOLD_EPISODE_GAP_MS = 12e4;
12872
12872
  async function channelInboundActivityAgeSecondsFor(codeName) {
12873
- const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-J3D7MRIU.js");
12873
+ const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-7CVB5NBT.js");
12874
12874
  const newest = newestPendingInboundActivityMtimeMs(dirname10(paneLogPath(codeName)));
12875
12875
  if (newest === null) return null;
12876
12876
  return Math.max(0, Math.floor((Date.now() - newest) / 1e3));
@@ -13530,7 +13530,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
13530
13530
  var lastVersionCheckAt = 0;
13531
13531
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
13532
13532
  var lastResponsivenessProbeAt = 0;
13533
- var agtCliVersion = true ? "0.28.772" : "dev";
13533
+ var agtCliVersion = true ? "0.28.774" : "dev";
13534
13534
  function resolveBrewPath(execFileSync2) {
13535
13535
  try {
13536
13536
  const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -15074,7 +15074,7 @@ function flushRestartedAgentDiagnostics(hostId, codeNames) {
15074
15074
  if (codeNames.length === 0) return;
15075
15075
  void (async () => {
15076
15076
  try {
15077
- const { collectDiagnostics } = await import("../persistent-session-NUVUR3PB.js");
15077
+ const { collectDiagnostics } = await import("../persistent-session-C2Y4CSAY.js");
15078
15078
  await api.post("/host/heartbeat", {
15079
15079
  host_id: hostId,
15080
15080
  agent_diagnostics: collectDiagnostics(codeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor)
@@ -15215,7 +15215,7 @@ async function pollCycleInner() {
15215
15215
  }
15216
15216
  try {
15217
15217
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
15218
- const { collectDiagnostics } = await import("../persistent-session-NUVUR3PB.js");
15218
+ const { collectDiagnostics } = await import("../persistent-session-C2Y4CSAY.js");
15219
15219
  const diagCodeNames = [...agentState.persistentSessionAgents];
15220
15220
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor) : void 0;
15221
15221
  let tailscaleHostname;
@@ -15384,7 +15384,7 @@ async function pollCycleInner() {
15384
15384
  collectPanelessActivityProbes,
15385
15385
  getResponsivenessIntervalMs,
15386
15386
  occupancyQualificationClassifications
15387
- } = await import("../responsiveness-probe-J3D7MRIU.js");
15387
+ } = await import("../responsiveness-probe-7CVB5NBT.js");
15388
15388
  const probeIntervalMs = getResponsivenessIntervalMs();
15389
15389
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
15390
15390
  const probeCodeNames = [...agentState.persistentSessionAgents];
@@ -15494,7 +15494,7 @@ async function pollCycleInner() {
15494
15494
  collectResponsivenessProbes,
15495
15495
  livePendingInboundOldestAgeSeconds,
15496
15496
  parkPendingInbound
15497
- } = await import("../responsiveness-probe-J3D7MRIU.js");
15497
+ } = await import("../responsiveness-probe-7CVB5NBT.js");
15498
15498
  const { getProjectDir: wedgeProjectDir } = await import("../scheduler-engine-NDP36U7O.js");
15499
15499
  const wedgeNow = /* @__PURE__ */ new Date();
15500
15500
  const liveAgents = agentState.persistentSessionAgents;
@@ -15650,7 +15650,7 @@ async function pollCycleInner() {
15650
15650
  }
15651
15651
  try {
15652
15652
  const { scrapeMcpFailedBannerCount } = await import("../pane-mcp-banner-scraper-JA437JIB.js");
15653
- const { probeSessionAuth } = await import("../session-auth-dead-GKZNZKWI.js");
15653
+ const { probeSessionAuth } = await import("../session-auth-dead-E3WEZYEH.js");
15654
15654
  const observations = [];
15655
15655
  const pendingCacheCommits = [];
15656
15656
  const modelApiErrorReportingOn = hostFlagStore().getBoolean("model-api-error-reporting");
@@ -19690,7 +19690,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
19690
19690
  void api.post("/host/restart-ack", { host_id: hostId, agent_id: agentId, restart_requested_at: requestedAt }).catch((err) => log(`[restart-lane] ack failed for '${codeName}': ${err.message}`));
19691
19691
  void (async () => {
19692
19692
  try {
19693
- const { collectDiagnostics } = await import("../persistent-session-NUVUR3PB.js");
19693
+ const { collectDiagnostics } = await import("../persistent-session-C2Y4CSAY.js");
19694
19694
  await api.post("/host/heartbeat", {
19695
19695
  host_id: hostId,
19696
19696
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor)
@@ -19736,7 +19736,7 @@ async function respawnAgentAfterMcpStop(codeName, reason) {
19736
19736
  }
19737
19737
  try {
19738
19738
  const hostId = await getHostId();
19739
- const { collectDiagnostics } = await import("../persistent-session-NUVUR3PB.js");
19739
+ const { collectDiagnostics } = await import("../persistent-session-C2Y4CSAY.js");
19740
19740
  await api.post("/host/heartbeat", {
19741
19741
  host_id: hostId,
19742
19742
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor)
@@ -20372,7 +20372,7 @@ async function processClaudePairSessions(agents) {
20372
20372
  killPairSession,
20373
20373
  pairTmuxSession,
20374
20374
  finalizeClaudePairOnboarding
20375
- } = await import("../claude-pair-runtime-Z7JFVKIY.js");
20375
+ } = await import("../claude-pair-runtime-O7T3PIMT.js");
20376
20376
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
20377
20377
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
20378
20378
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -37139,6 +37139,74 @@ var FLAG_REGISTRY = [
37139
37139
  // dominant callers. This is precisely the deliberate, audited flip ADR-0022
37140
37140
  // §4 reserves confirmation for.
37141
37141
  sensitive: true
37142
+ },
37143
+ {
37144
+ key: "artefact-export-jobs-serve",
37145
+ description: 'ADR-0077 / ENG-9729 \u2014 the MASTER switch for the async artefact-export lane. ON (default) = a route may resolve an export request to an artefact_export_jobs row: attach to one that is already rendering, or hand back the bytes of one that has completed. OFF = the job lane is invisible and every export renders synchronously, which is the pre-ADR-0077 behaviour. DEFAULT true DOES NOT MEAN ASYNC IS ON. Nothing CREATES a job unless artefact-export-jobs-start is also on, so with the shipped defaults this flag governs an empty table and no caller can tell it exists. WHY THE LANE IS TWO FLAGS AND NOT ONE. Disarming the start lever while renders are in flight must not strand the callers holding their job ids: those renders keep running (a Lambda invocation nobody awaits still runs to completion), and their bytes land in the private bucket regardless. A single flag would take the only door to them away at the moment it was most needed. So "stop creating new jobs" and "keep serving the ones that exist" are separate, and this is the second one. READ-ONLY BY CONSTRUCTION: with start off, the route looks up a REUSABLE row and nothing more \u2014 it never inserts, never requeues a terminal row, and never dispatches a renderer. A requeue mints an attempt and fires an invoke, so a serve lever that requeued would start new renders under a disarmed start lever, which is the exact thing disarming it was meant to stop. Evaluated server-side per request (org-scoped), so a flip applies within the evaluation cache TTL with no deploy and no host update. Deliberately NO envVar: the consumer is the API Lambda, whose environment block is at the AWS 4KB cap (ENG-7972), and a literal in sst.config.ts would pin every stage and make the admin control inert (ENG-8303).',
37146
+ flagType: "boolean",
37147
+ // ON is the no-op default here, which looks like the wrong direction until
37148
+ // you read it with the start lever: serving an empty table is
37149
+ // indistinguishable from not serving at all. Shipping it OFF would instead
37150
+ // mean that arming async later takes TWO flips, and that the second one is
37151
+ // the easy one to forget — leaving a fleet that creates jobs it refuses to
37152
+ // serve, i.e. every slow export failing while the flag page reads "async: on".
37153
+ defaultValue: true
37154
+ },
37155
+ {
37156
+ key: "artefact-export-jobs-start",
37157
+ description: "ADR-0077 / ENG-9729 \u2014 whether a route may CREATE an async artefact-export job: insert or requeue an artefact_export_jobs row, dispatch the worker, and answer a caller with a job id instead of bytes when the ~20s grace window is exceeded. OFF (default) = today's synchronous render on every export. THIS IS THE ROLLOUT GATE, AND IT IS ORDERED AGAINST A CLIENT, NOT A DEPLOY. The agent caller is `artefacts_download`, which ships inside the agt CLI to a permanently mixed-version fleet \u2014 hosts self-update overnight and some never do. Arming this against a CLI that cannot read a job-id body does not degrade an export, it BREAKS one: the pre-ENG-9729 tool does Buffer.from(data.base64) with no branch for any other shape, so a job id arrives as a TypeError deep in file-write logic. That is strictly worse than the 503 + Retry-After it would replace, which the tool already understands. A published CLI version is NOT evidence of convergence \u2014 measure hosts.agt_version across recently-seen hosts first. The procedure is docs/runbooks/async-export-rollout.md; arm this only after running it. Turning it back OFF stops new jobs and leaves artefact-export-jobs-serve delivering the in-flight ones, which is why the two are separate. Org-scoped, so it can be armed for one tenant at a time. Deliberately NO envVar (ENG-7972 4KB cap, ENG-8303 pin).",
37158
+ flagType: "boolean",
37159
+ // Dark. The client half of this change publishes with the same merge, and
37160
+ // the fleet has not converged on it at merge time by construction — see the
37161
+ // runbook. Arming is an operator action taken against a measurement.
37162
+ defaultValue: false,
37163
+ // Arming this changes the RESPONSE SHAPE of a route whose callers include a
37164
+ // binary we do not control the version of. ADR-0022 section 4 reserves
37165
+ // confirmation for exactly this: a flip that a single unguarded write should
37166
+ // not be able to make.
37167
+ sensitive: true,
37168
+ // `since` USED SLIGHTLY OFF-LABEL, AND THAT IS THE POINT.
37169
+ //
37170
+ // The field's docblock says it records the CLI whose build first shipped
37171
+ // this flag's HOST-SIDE READER, and this flag has none — it is evaluated
37172
+ // server-side in the export route, so every host "honors" a flip the instant
37173
+ // it is made. Read literally, `since` should therefore be undefined.
37174
+ //
37175
+ // Undefined is the dangerous answer. `classifyHostReach` returns `honors`
37176
+ // for every host when `since` is unset, so the flip-reach modal — the screen
37177
+ // an operator is looking at AT THE MOMENT OF THE FLIP — would report full
37178
+ // reach for the one flag whose entire purpose is "do not flip this until you
37179
+ // have measured the fleet". It would be telling the truth about the
37180
+ // mechanism (the flip does reach every host immediately) while giving
37181
+ // exactly the wrong answer to the question being asked (can those hosts
37182
+ // survive it). The dependency is real; it is on the CLI's ability to SURVIVE
37183
+ // the flag, not to READ it, and the modal cannot tell those apart.
37184
+ //
37185
+ // Pinned ABOVE every published release on purpose, the `agt-bin-path`
37186
+ // precedent: it classifies EVERY host as `stale`, so the modal reports no
37187
+ // reach rather than reach it has no basis for. A guessed number would be
37188
+ // worse than a placeholder, and the two directions are not symmetric —
37189
+ // too HIGH over-warns (annoying), too LOW promises reach to a build that
37190
+ // answers a job id with a TypeError inside file-write logic (a broken export
37191
+ // reported to the agent as "download failed").
37192
+ //
37193
+ // The exact version cannot be known here: the CLI auto-publishes ON MERGE of
37194
+ // this change, patch-bumping from whatever is current, so any pre-merge
37195
+ // value is a guess. Do NOT read it off `apps/cli/package.json`, and do NOT
37196
+ // trust a version produced by re-running a failed publish job (ENG-9647: a
37197
+ // re-run rebuilds the ORIGINAL tree under a HIGHER number, so the version
37198
+ // string would claim a capability the bytes do not carry).
37199
+ //
37200
+ // AND `since` IS ADVISORY, NOT A GUARD. It is read in exactly one place —
37201
+ // the flip-reach modal. Nothing in the evaluation or override-write path
37202
+ // consults it, so this changes what an operator is TOLD and does not stop a
37203
+ // flip on an under-`since` host. What holds the line is `defaultValue:
37204
+ // false` + `sensitive: true` + the runbook.
37205
+ //
37206
+ // REPLACING IT IS STEP 1 OF THE ARMING PROCEDURE in
37207
+ // docs/runbooks/async-export-rollout.md, so the placeholder has a named
37208
+ // owner and a named moment rather than being a deferral nobody holds.
37209
+ since: "0.29.0"
37142
37210
  }
37143
37211
  ];
37144
37212
  var REGISTRY_BY_KEY = new Map(FLAG_REGISTRY.map((definition) => [definition.key, definition]));
package/dist/mcp/index.js CHANGED
@@ -36790,6 +36790,60 @@ function jsonSchemaPropertyToZod(prop) {
36790
36790
  return external_exports.unknown();
36791
36791
  }
36792
36792
 
36793
+ // src/artefact-export-response.ts
36794
+ var DEFAULT_EXPORT_RETRY_MS = 1e4;
36795
+ function classifyExportResponse(body) {
36796
+ if (typeof body !== "object" || body === null) {
36797
+ throw new Error("export response was not an object");
36798
+ }
36799
+ const candidate = body;
36800
+ if (typeof candidate.job_id === "string" && candidate.job_id.length > 0) {
36801
+ const retry = typeof candidate.retry_after_ms === "number" && Number.isFinite(candidate.retry_after_ms) ? Math.max(0, candidate.retry_after_ms) : null;
36802
+ return {
36803
+ kind: "pending",
36804
+ jobId: candidate.job_id,
36805
+ status: typeof candidate.status === "string" ? candidate.status : null,
36806
+ retryAfterMs: retry
36807
+ };
36808
+ }
36809
+ if (typeof candidate.base64 !== "string" || candidate.base64.length === 0) {
36810
+ throw new Error("export response carried neither rendered bytes nor a job id");
36811
+ }
36812
+ return {
36813
+ kind: "bytes",
36814
+ body: {
36815
+ base64: candidate.base64,
36816
+ content_type: typeof candidate.content_type === "string" ? candidate.content_type : "",
36817
+ filename: typeof candidate.filename === "string" ? candidate.filename : "",
36818
+ format: typeof candidate.format === "string" ? candidate.format : "",
36819
+ version: typeof candidate.version === "number" ? candidate.version : 0
36820
+ }
36821
+ };
36822
+ }
36823
+ function pendingExportText(n) {
36824
+ const wait = Math.max(1, Math.round((n.retryAfterMs ?? DEFAULT_EXPORT_RETRY_MS) / 1e3));
36825
+ const fmt = n.format.toUpperCase();
36826
+ const retryLine = `Call artefacts_download again with the SAME arguments in about ${wait}s. The retry attaches to the render already in progress rather than starting a new one, and items that have already finished come back immediately.`;
36827
+ if (n.totalItems <= 1) {
36828
+ return [
36829
+ `The ${fmt} export is still rendering (job ${n.jobId}); no file was saved yet.`,
36830
+ retryLine,
36831
+ "Nothing has failed \u2014 this document simply takes longer than the export request waits."
36832
+ ].join("\n");
36833
+ }
36834
+ const header = n.saved.length > 0 ? `Rendered ${n.saved.length} of ${n.totalItems} items; item ${n.pendingPosition} is still rendering (job ${n.jobId}), so this is a PARTIAL export.` : `None of the ${n.totalItems} items finished in time; item ${n.pendingPosition} is still rendering (job ${n.jobId}).`;
36835
+ const lines = [header];
36836
+ if (n.saved.length > 0) {
36837
+ lines.push(`Saved so far (items 1-${n.saved.length}):`);
36838
+ lines.push(...n.saved.map((p2) => `- ${p2}`));
36839
+ lines.push(
36840
+ "These files are complete and usable, but they are NOT the whole document \u2014 do not send them as if they were."
36841
+ );
36842
+ }
36843
+ lines.push(retryLine);
36844
+ return lines.join("\n");
36845
+ }
36846
+
36793
36847
  // src/mutation-commit-state.ts
36794
36848
  var API_GATEWAY_INTEGRATION_TIMEOUT_MS = 29e3;
36795
36849
  var GATEWAY_TIMEOUT_MARGIN_MS = 3e3;
@@ -44134,6 +44188,74 @@ var FLAG_REGISTRY = [
44134
44188
  // dominant callers. This is precisely the deliberate, audited flip ADR-0022
44135
44189
  // §4 reserves confirmation for.
44136
44190
  sensitive: true
44191
+ },
44192
+ {
44193
+ key: "artefact-export-jobs-serve",
44194
+ description: 'ADR-0077 / ENG-9729 \u2014 the MASTER switch for the async artefact-export lane. ON (default) = a route may resolve an export request to an artefact_export_jobs row: attach to one that is already rendering, or hand back the bytes of one that has completed. OFF = the job lane is invisible and every export renders synchronously, which is the pre-ADR-0077 behaviour. DEFAULT true DOES NOT MEAN ASYNC IS ON. Nothing CREATES a job unless artefact-export-jobs-start is also on, so with the shipped defaults this flag governs an empty table and no caller can tell it exists. WHY THE LANE IS TWO FLAGS AND NOT ONE. Disarming the start lever while renders are in flight must not strand the callers holding their job ids: those renders keep running (a Lambda invocation nobody awaits still runs to completion), and their bytes land in the private bucket regardless. A single flag would take the only door to them away at the moment it was most needed. So "stop creating new jobs" and "keep serving the ones that exist" are separate, and this is the second one. READ-ONLY BY CONSTRUCTION: with start off, the route looks up a REUSABLE row and nothing more \u2014 it never inserts, never requeues a terminal row, and never dispatches a renderer. A requeue mints an attempt and fires an invoke, so a serve lever that requeued would start new renders under a disarmed start lever, which is the exact thing disarming it was meant to stop. Evaluated server-side per request (org-scoped), so a flip applies within the evaluation cache TTL with no deploy and no host update. Deliberately NO envVar: the consumer is the API Lambda, whose environment block is at the AWS 4KB cap (ENG-7972), and a literal in sst.config.ts would pin every stage and make the admin control inert (ENG-8303).',
44195
+ flagType: "boolean",
44196
+ // ON is the no-op default here, which looks like the wrong direction until
44197
+ // you read it with the start lever: serving an empty table is
44198
+ // indistinguishable from not serving at all. Shipping it OFF would instead
44199
+ // mean that arming async later takes TWO flips, and that the second one is
44200
+ // the easy one to forget — leaving a fleet that creates jobs it refuses to
44201
+ // serve, i.e. every slow export failing while the flag page reads "async: on".
44202
+ defaultValue: true
44203
+ },
44204
+ {
44205
+ key: "artefact-export-jobs-start",
44206
+ description: "ADR-0077 / ENG-9729 \u2014 whether a route may CREATE an async artefact-export job: insert or requeue an artefact_export_jobs row, dispatch the worker, and answer a caller with a job id instead of bytes when the ~20s grace window is exceeded. OFF (default) = today's synchronous render on every export. THIS IS THE ROLLOUT GATE, AND IT IS ORDERED AGAINST A CLIENT, NOT A DEPLOY. The agent caller is `artefacts_download`, which ships inside the agt CLI to a permanently mixed-version fleet \u2014 hosts self-update overnight and some never do. Arming this against a CLI that cannot read a job-id body does not degrade an export, it BREAKS one: the pre-ENG-9729 tool does Buffer.from(data.base64) with no branch for any other shape, so a job id arrives as a TypeError deep in file-write logic. That is strictly worse than the 503 + Retry-After it would replace, which the tool already understands. A published CLI version is NOT evidence of convergence \u2014 measure hosts.agt_version across recently-seen hosts first. The procedure is docs/runbooks/async-export-rollout.md; arm this only after running it. Turning it back OFF stops new jobs and leaves artefact-export-jobs-serve delivering the in-flight ones, which is why the two are separate. Org-scoped, so it can be armed for one tenant at a time. Deliberately NO envVar (ENG-7972 4KB cap, ENG-8303 pin).",
44207
+ flagType: "boolean",
44208
+ // Dark. The client half of this change publishes with the same merge, and
44209
+ // the fleet has not converged on it at merge time by construction — see the
44210
+ // runbook. Arming is an operator action taken against a measurement.
44211
+ defaultValue: false,
44212
+ // Arming this changes the RESPONSE SHAPE of a route whose callers include a
44213
+ // binary we do not control the version of. ADR-0022 section 4 reserves
44214
+ // confirmation for exactly this: a flip that a single unguarded write should
44215
+ // not be able to make.
44216
+ sensitive: true,
44217
+ // `since` USED SLIGHTLY OFF-LABEL, AND THAT IS THE POINT.
44218
+ //
44219
+ // The field's docblock says it records the CLI whose build first shipped
44220
+ // this flag's HOST-SIDE READER, and this flag has none — it is evaluated
44221
+ // server-side in the export route, so every host "honors" a flip the instant
44222
+ // it is made. Read literally, `since` should therefore be undefined.
44223
+ //
44224
+ // Undefined is the dangerous answer. `classifyHostReach` returns `honors`
44225
+ // for every host when `since` is unset, so the flip-reach modal — the screen
44226
+ // an operator is looking at AT THE MOMENT OF THE FLIP — would report full
44227
+ // reach for the one flag whose entire purpose is "do not flip this until you
44228
+ // have measured the fleet". It would be telling the truth about the
44229
+ // mechanism (the flip does reach every host immediately) while giving
44230
+ // exactly the wrong answer to the question being asked (can those hosts
44231
+ // survive it). The dependency is real; it is on the CLI's ability to SURVIVE
44232
+ // the flag, not to READ it, and the modal cannot tell those apart.
44233
+ //
44234
+ // Pinned ABOVE every published release on purpose, the `agt-bin-path`
44235
+ // precedent: it classifies EVERY host as `stale`, so the modal reports no
44236
+ // reach rather than reach it has no basis for. A guessed number would be
44237
+ // worse than a placeholder, and the two directions are not symmetric —
44238
+ // too HIGH over-warns (annoying), too LOW promises reach to a build that
44239
+ // answers a job id with a TypeError inside file-write logic (a broken export
44240
+ // reported to the agent as "download failed").
44241
+ //
44242
+ // The exact version cannot be known here: the CLI auto-publishes ON MERGE of
44243
+ // this change, patch-bumping from whatever is current, so any pre-merge
44244
+ // value is a guess. Do NOT read it off `apps/cli/package.json`, and do NOT
44245
+ // trust a version produced by re-running a failed publish job (ENG-9647: a
44246
+ // re-run rebuilds the ORIGINAL tree under a HIGHER number, so the version
44247
+ // string would claim a capability the bytes do not carry).
44248
+ //
44249
+ // AND `since` IS ADVISORY, NOT A GUARD. It is read in exactly one place —
44250
+ // the flip-reach modal. Nothing in the evaluation or override-write path
44251
+ // consults it, so this changes what an operator is TOLD and does not stop a
44252
+ // flip on an under-`since` host. What holds the line is `defaultValue:
44253
+ // false` + `sensitive: true` + the runbook.
44254
+ //
44255
+ // REPLACING IT IS STEP 1 OF THE ARMING PROCEDURE in
44256
+ // docs/runbooks/async-export-rollout.md, so the placeholder has a named
44257
+ // owner and a named moment rather than being a deferral nobody holds.
44258
+ since: "0.29.0"
44137
44259
  }
44138
44260
  ];
44139
44261
  var REGISTRY_BY_KEY = new Map(FLAG_REGISTRY.map((definition) => [definition.key, definition]));
@@ -46523,6 +46645,54 @@ server.tool(
46523
46645
  return { content: [{ type: "text", text: lines.join("\n") }] };
46524
46646
  }
46525
46647
  );
46648
+ server.tool(
46649
+ "rule_list",
46650
+ "List the standing rules currently in force for you \u2014 the short instructions your people have asked you to follow on EVERY turn, rendered into your instructions verbatim. Use this before adding a rule (so you know whether you are at the limit and what you would be replacing), and whenever someone asks what rules you are operating under. Rules are not task-specific and are not skills: a skill loads only when its description matches the task, a rule applies always.",
46651
+ {},
46652
+ async () => {
46653
+ const data = await apiGet(`/host/agent-rules?agent_id=${encodeURIComponent(AGT_AGENT_ID ?? "")}`);
46654
+ if (!data.ok) {
46655
+ return { content: [{ type: "text", text: "Could not read your standing rules." }], isError: true };
46656
+ }
46657
+ if (!data.rules.length) {
46658
+ return {
46659
+ content: [
46660
+ { type: "text", text: `No standing rules yet. You can hold up to ${data.max}, each up to ${data.max_chars} characters.` }
46661
+ ]
46662
+ };
46663
+ }
46664
+ const lines = data.rules.map((r) => `- ${r.text} (id: ${r.id})`).join("\n");
46665
+ return {
46666
+ content: [
46667
+ { type: "text", text: `${data.rules.length} of ${data.max} standing rules in force:
46668
+ ${lines}` }
46669
+ ]
46670
+ };
46671
+ }
46672
+ );
46673
+ server.tool(
46674
+ "rule_add",
46675
+ 'Turn a repeated instruction into a STANDING RULE that applies on every turn. Use when someone asks for the same thing more than once, or phrases it as a standing instruction ("always...", "every time...", "from now on..."). Propose it in plain language first and get a yes \u2014 never create one silently, and never make the person learn the word "skill" or "rule" to get it. Keep it SHORT: a rule is resident in your instructions on every single request, so if it needs more than a sentence it is really a skill (skill_create), which costs nothing until the task comes up. If you are already at the limit, pass replace_id with the id of the rule this one supersedes (rule_list shows the ids) \u2014 and tell the person what you dropped, in the same message. There is deliberately no way to remove a rule without adding one in its place: a rule that quietly stops applying is worse than one that was never made, because nobody notices.',
46676
+ {
46677
+ text: external_exports.string().describe("The rule, in the person's own words where possible. One sentence. It applies on every turn, so write it as a standing instruction rather than a task."),
46678
+ replace_id: external_exports.string().optional().describe("The id of an existing rule this one replaces (from rule_list). Required once you are at the limit. The person MUST be told which rule was dropped, in the same reply.")
46679
+ },
46680
+ async (params) => {
46681
+ const data = await apiPost("/host/agent-rules", {
46682
+ agent_id: AGT_AGENT_ID,
46683
+ text: params.text,
46684
+ ...params.replace_id ? { replace_id: params.replace_id } : {}
46685
+ });
46686
+ if (!data.ok) {
46687
+ return {
46688
+ content: [{ type: "text", text: data.message ?? "Could not save that rule." }],
46689
+ isError: true
46690
+ };
46691
+ }
46692
+ const said = data.evicted ? `Added the rule. To make room I dropped: "${data.evicted.text}". Tell the person BOTH of these, not just the addition.` : `Added the rule: "${data.rule?.text}".`;
46693
+ return { content: [{ type: "text", text: said }] };
46694
+ }
46695
+ );
46526
46696
  server.tool(
46527
46697
  "knowledge_add",
46528
46698
  'Save a new durable fact or learning to the knowledge base. Use when the user explicitly asks you to save/remember something, or when you discover information that will be useful on future runs (e.g. a tenant id, an API key name, a company policy). Do NOT use for tasks (use kanban_add) or trivial one-off answers. Defaults to TEAM scope; pass scope:"org" only when the user asks for organization/company-wide knowledge (visible to every team in the org). Returns the new entry id so you can target it with knowledge_update / knowledge_delete.',
@@ -47037,6 +47207,11 @@ var LOCAL_TOOL_NAMES = /* @__PURE__ */ new Set([
47037
47207
  "kanban_reopen",
47038
47208
  // ENG-9375: the READ path over a teammate's board (counts only).
47039
47209
  "kanban_peek",
47210
+ // ENG-9789: per-agent standing rules. No rule_remove by design — see the
47211
+ // registration below; an agent may only displace a rule by naming it while
47212
+ // adding another.
47213
+ "rule_list",
47214
+ "rule_add",
47040
47215
  // ENG-6953: agent + people directory lookup (always registered locally).
47041
47216
  "directory_lookup",
47042
47217
  "status_standup",
@@ -47398,21 +47573,36 @@ async function exportArtefactToFile(dir, artefactId, format, item) {
47398
47573
  // it, the gateway replaces our authored error with its opaque envelope and
47399
47574
  // we lose the ability to say whether the render landed.
47400
47575
  //
47401
- // NOT a readiness signal. Raising the budget does not make this caller able
47402
- // to consume ADR-0077's async response: the `data.base64` read just below
47403
- // has no branch for a `{ job_id }` body and would throw on one. So a fleet
47404
- // fully updated to this CLI is converged on the TIMEOUT and no more ready
47405
- // for the RESPONSE than before. The server's async path must stay dark for
47406
- // this route until job-response handling ships here (ENG-9729) — reading a
47407
- // published version number as evidence of a capability it does not carry is
47408
- // exactly the mistake this comment's first paragraph describes.
47576
+ // ENG-9729 closed the readiness gap this comment used to describe: the
47577
+ // response is now classified before anything reads `base64`, so a `{ job_id }`
47578
+ // body is handled rather than thrown on. The budget above is what makes the
47579
+ // server's ~20s grace window reachable from here at all.
47580
+ //
47581
+ // WHAT IS STILL NOT TRUE: a published CLI version is not convergence. This
47582
+ // build can read a job body; the fleet is a mixed-version population and
47583
+ // hosts update overnight, some never. The server's start lever
47584
+ // (`artefact-export-jobs-start`) must therefore stay off until
47585
+ // hosts.agt_version has been MEASURED — docs/runbooks/async-export-rollout.md
47586
+ // — because arming it against an older CLI does not degrade an export, it
47587
+ // throws a TypeError inside file-write logic, which is strictly worse than
47588
+ // the 503 + Retry-After that build already understands.
47409
47589
  FORWARD_TOOL_CALL_TIMEOUT_MS
47410
47590
  );
47591
+ const outcome = classifyExportResponse(data);
47592
+ if (outcome.kind === "pending") {
47593
+ return { kind: "pending", jobId: outcome.jobId, retryAfterMs: outcome.retryAfterMs };
47594
+ }
47411
47595
  const fallback = `${artefactId}${item !== void 0 ? `-item-${item + 1}` : ""}.${format}`;
47412
- const filePath = join4(dir, safeDownloadName(data.filename, fallback));
47413
- const buf = Buffer.from(data.base64, "base64");
47596
+ const filePath = join4(dir, safeDownloadName(outcome.body.filename, fallback));
47597
+ const buf = Buffer.from(outcome.body.base64, "base64");
47414
47598
  await writeFile(filePath, buf);
47415
- return { path: filePath, bytes: buf.length, contentType: data.content_type, version: data.version };
47599
+ return {
47600
+ kind: "file",
47601
+ path: filePath,
47602
+ bytes: buf.length,
47603
+ contentType: outcome.body.content_type,
47604
+ version: outcome.body.version
47605
+ };
47416
47606
  }
47417
47607
  async function resolveItemCountForPng(artefactId) {
47418
47608
  try {
@@ -47445,8 +47635,18 @@ async function downloadArtefactText(params) {
47445
47635
  const saved = [];
47446
47636
  let contentType = "";
47447
47637
  let version2 = 0;
47448
- for (const item of items) {
47449
- const r = await exportArtefactToFile(dir, params.artefact_id, params.format, item);
47638
+ for (let i = 0; i < items.length; i++) {
47639
+ const r = await exportArtefactToFile(dir, params.artefact_id, params.format, items[i]);
47640
+ if (r.kind === "pending") {
47641
+ return pendingExportText({
47642
+ format: params.format,
47643
+ saved: saved.map((f) => f.path),
47644
+ totalItems: items.length,
47645
+ pendingPosition: i + 1,
47646
+ jobId: r.jobId,
47647
+ retryAfterMs: r.retryAfterMs
47648
+ });
47649
+ }
47450
47650
  saved.push({ path: r.path, bytes: r.bytes });
47451
47651
  contentType = r.contentType;
47452
47652
  version2 = r.version;
@@ -42863,6 +42863,74 @@ var FLAG_REGISTRY = [
42863
42863
  // dominant callers. This is precisely the deliberate, audited flip ADR-0022
42864
42864
  // §4 reserves confirmation for.
42865
42865
  sensitive: true
42866
+ },
42867
+ {
42868
+ key: "artefact-export-jobs-serve",
42869
+ description: 'ADR-0077 / ENG-9729 \u2014 the MASTER switch for the async artefact-export lane. ON (default) = a route may resolve an export request to an artefact_export_jobs row: attach to one that is already rendering, or hand back the bytes of one that has completed. OFF = the job lane is invisible and every export renders synchronously, which is the pre-ADR-0077 behaviour. DEFAULT true DOES NOT MEAN ASYNC IS ON. Nothing CREATES a job unless artefact-export-jobs-start is also on, so with the shipped defaults this flag governs an empty table and no caller can tell it exists. WHY THE LANE IS TWO FLAGS AND NOT ONE. Disarming the start lever while renders are in flight must not strand the callers holding their job ids: those renders keep running (a Lambda invocation nobody awaits still runs to completion), and their bytes land in the private bucket regardless. A single flag would take the only door to them away at the moment it was most needed. So "stop creating new jobs" and "keep serving the ones that exist" are separate, and this is the second one. READ-ONLY BY CONSTRUCTION: with start off, the route looks up a REUSABLE row and nothing more \u2014 it never inserts, never requeues a terminal row, and never dispatches a renderer. A requeue mints an attempt and fires an invoke, so a serve lever that requeued would start new renders under a disarmed start lever, which is the exact thing disarming it was meant to stop. Evaluated server-side per request (org-scoped), so a flip applies within the evaluation cache TTL with no deploy and no host update. Deliberately NO envVar: the consumer is the API Lambda, whose environment block is at the AWS 4KB cap (ENG-7972), and a literal in sst.config.ts would pin every stage and make the admin control inert (ENG-8303).',
42870
+ flagType: "boolean",
42871
+ // ON is the no-op default here, which looks like the wrong direction until
42872
+ // you read it with the start lever: serving an empty table is
42873
+ // indistinguishable from not serving at all. Shipping it OFF would instead
42874
+ // mean that arming async later takes TWO flips, and that the second one is
42875
+ // the easy one to forget — leaving a fleet that creates jobs it refuses to
42876
+ // serve, i.e. every slow export failing while the flag page reads "async: on".
42877
+ defaultValue: true
42878
+ },
42879
+ {
42880
+ key: "artefact-export-jobs-start",
42881
+ description: "ADR-0077 / ENG-9729 \u2014 whether a route may CREATE an async artefact-export job: insert or requeue an artefact_export_jobs row, dispatch the worker, and answer a caller with a job id instead of bytes when the ~20s grace window is exceeded. OFF (default) = today's synchronous render on every export. THIS IS THE ROLLOUT GATE, AND IT IS ORDERED AGAINST A CLIENT, NOT A DEPLOY. The agent caller is `artefacts_download`, which ships inside the agt CLI to a permanently mixed-version fleet \u2014 hosts self-update overnight and some never do. Arming this against a CLI that cannot read a job-id body does not degrade an export, it BREAKS one: the pre-ENG-9729 tool does Buffer.from(data.base64) with no branch for any other shape, so a job id arrives as a TypeError deep in file-write logic. That is strictly worse than the 503 + Retry-After it would replace, which the tool already understands. A published CLI version is NOT evidence of convergence \u2014 measure hosts.agt_version across recently-seen hosts first. The procedure is docs/runbooks/async-export-rollout.md; arm this only after running it. Turning it back OFF stops new jobs and leaves artefact-export-jobs-serve delivering the in-flight ones, which is why the two are separate. Org-scoped, so it can be armed for one tenant at a time. Deliberately NO envVar (ENG-7972 4KB cap, ENG-8303 pin).",
42882
+ flagType: "boolean",
42883
+ // Dark. The client half of this change publishes with the same merge, and
42884
+ // the fleet has not converged on it at merge time by construction — see the
42885
+ // runbook. Arming is an operator action taken against a measurement.
42886
+ defaultValue: false,
42887
+ // Arming this changes the RESPONSE SHAPE of a route whose callers include a
42888
+ // binary we do not control the version of. ADR-0022 section 4 reserves
42889
+ // confirmation for exactly this: a flip that a single unguarded write should
42890
+ // not be able to make.
42891
+ sensitive: true,
42892
+ // `since` USED SLIGHTLY OFF-LABEL, AND THAT IS THE POINT.
42893
+ //
42894
+ // The field's docblock says it records the CLI whose build first shipped
42895
+ // this flag's HOST-SIDE READER, and this flag has none — it is evaluated
42896
+ // server-side in the export route, so every host "honors" a flip the instant
42897
+ // it is made. Read literally, `since` should therefore be undefined.
42898
+ //
42899
+ // Undefined is the dangerous answer. `classifyHostReach` returns `honors`
42900
+ // for every host when `since` is unset, so the flip-reach modal — the screen
42901
+ // an operator is looking at AT THE MOMENT OF THE FLIP — would report full
42902
+ // reach for the one flag whose entire purpose is "do not flip this until you
42903
+ // have measured the fleet". It would be telling the truth about the
42904
+ // mechanism (the flip does reach every host immediately) while giving
42905
+ // exactly the wrong answer to the question being asked (can those hosts
42906
+ // survive it). The dependency is real; it is on the CLI's ability to SURVIVE
42907
+ // the flag, not to READ it, and the modal cannot tell those apart.
42908
+ //
42909
+ // Pinned ABOVE every published release on purpose, the `agt-bin-path`
42910
+ // precedent: it classifies EVERY host as `stale`, so the modal reports no
42911
+ // reach rather than reach it has no basis for. A guessed number would be
42912
+ // worse than a placeholder, and the two directions are not symmetric —
42913
+ // too HIGH over-warns (annoying), too LOW promises reach to a build that
42914
+ // answers a job id with a TypeError inside file-write logic (a broken export
42915
+ // reported to the agent as "download failed").
42916
+ //
42917
+ // The exact version cannot be known here: the CLI auto-publishes ON MERGE of
42918
+ // this change, patch-bumping from whatever is current, so any pre-merge
42919
+ // value is a guess. Do NOT read it off `apps/cli/package.json`, and do NOT
42920
+ // trust a version produced by re-running a failed publish job (ENG-9647: a
42921
+ // re-run rebuilds the ORIGINAL tree under a HIGHER number, so the version
42922
+ // string would claim a capability the bytes do not carry).
42923
+ //
42924
+ // AND `since` IS ADVISORY, NOT A GUARD. It is read in exactly one place —
42925
+ // the flip-reach modal. Nothing in the evaluation or override-write path
42926
+ // consults it, so this changes what an operator is TOLD and does not stop a
42927
+ // flip on an under-`since` host. What holds the line is `defaultValue:
42928
+ // false` + `sensitive: true` + the runbook.
42929
+ //
42930
+ // REPLACING IT IS STEP 1 OF THE ARMING PROCEDURE in
42931
+ // docs/runbooks/async-export-rollout.md, so the placeholder has a named
42932
+ // owner and a named moment rather than being a deferral nobody holds.
42933
+ since: "0.29.0"
42866
42934
  }
42867
42935
  ];
42868
42936
  var REGISTRY_BY_KEY = new Map(FLAG_REGISTRY.map((definition) => [definition.key, definition]));