@fastagent-sh/fastagent 0.19.0 → 0.20.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 (131) hide show
  1. package/dist/atomic-write.d.ts +11 -7
  2. package/dist/atomic-write.js +16 -11
  3. package/dist/channels/agentcore-limits.d.ts +9 -0
  4. package/dist/channels/agentcore-limits.js +9 -0
  5. package/dist/channels/agentcore-service.d.ts +12 -11
  6. package/dist/channels/agentcore-service.js +14 -18
  7. package/dist/channels/agentcore.d.ts +11 -19
  8. package/dist/channels/agentcore.js +14 -7
  9. package/dist/channels/control.d.ts +50 -12
  10. package/dist/channels/control.js +239 -96
  11. package/dist/channels/discover.d.ts +26 -4
  12. package/dist/channels/discover.js +12 -23
  13. package/dist/channels/feishu/feishu-api.js +5 -8
  14. package/dist/channels/feishu/feishu.js +35 -18
  15. package/dist/channels/kit/attachment-path.d.ts +12 -0
  16. package/dist/channels/kit/attachment-path.js +43 -0
  17. package/dist/channels/kit/context-buffer.d.ts +9 -0
  18. package/dist/channels/kit/context-buffer.js +11 -0
  19. package/dist/channels/kit/signature.d.ts +12 -0
  20. package/dist/channels/kit/signature.js +17 -0
  21. package/dist/channels/kit/stop-command.js +3 -3
  22. package/dist/channels/kit/tasks.d.ts +1 -1
  23. package/dist/channels/kit/tasks.js +10 -4
  24. package/dist/channels/kit/turn-store.d.ts +19 -3
  25. package/dist/channels/kit/turn-store.js +29 -2
  26. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  27. package/dist/channels/slack/invoke-turn.js +5 -1
  28. package/dist/channels/slack/onboard.js +5 -5
  29. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  30. package/dist/channels/slack/onboarding-state.js +13 -20
  31. package/dist/channels/slack/register-webhook.js +1 -1
  32. package/dist/channels/slack/scaffold/slack-send.ts +2 -17
  33. package/dist/channels/slack/slack-api.js +22 -54
  34. package/dist/channels/slack/slack.js +13 -13
  35. package/dist/channels/telegram/parse.d.ts +21 -1
  36. package/dist/channels/telegram/parse.js +65 -11
  37. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  38. package/dist/channels/telegram/telegram-api.js +5 -6
  39. package/dist/channels/telegram/telegram.d.ts +2 -2
  40. package/dist/channels/telegram/telegram.js +57 -69
  41. package/dist/cli/add-slack.js +6 -5
  42. package/dist/cli/commands/add.js +1 -5
  43. package/dist/cli/commands/attach.d.ts +2 -4
  44. package/dist/cli/commands/attach.js +17 -15
  45. package/dist/cli/commands/deploy.d.ts +50 -5
  46. package/dist/cli/commands/deploy.js +183 -134
  47. package/dist/cli/commands/dev.js +4 -31
  48. package/dist/cli/commands/info.js +1 -2
  49. package/dist/cli/commands/init.js +1 -2
  50. package/dist/cli/commands/start.js +31 -41
  51. package/dist/cli/serve.d.ts +12 -21
  52. package/dist/cli/serve.js +20 -25
  53. package/dist/cli/shared.d.ts +40 -10
  54. package/dist/cli/shared.js +51 -4
  55. package/dist/deploy/agentcore/logs.d.ts +2 -2
  56. package/dist/deploy/agentcore/logs.js +2 -2
  57. package/dist/deploy/agentcore/plan.d.ts +12 -10
  58. package/dist/deploy/agentcore/plan.js +39 -31
  59. package/dist/deploy/agentcore/run.d.ts +5 -4
  60. package/dist/deploy/agentcore/run.js +13 -36
  61. package/dist/deploy/channel-ingress.d.ts +73 -0
  62. package/dist/deploy/channel-ingress.js +101 -0
  63. package/dist/deploy/docker/plan.d.ts +3 -7
  64. package/dist/deploy/docker/plan.js +4 -15
  65. package/dist/deploy/docker/run.d.ts +18 -1
  66. package/dist/deploy/docker/run.js +10 -0
  67. package/dist/deploy/fly/plan.d.ts +4 -5
  68. package/dist/deploy/fly/plan.js +7 -22
  69. package/dist/deploy/fly/run.d.ts +9 -23
  70. package/dist/deploy/fly/run.js +32 -68
  71. package/dist/deploy/preflight.d.ts +6 -7
  72. package/dist/deploy/preflight.js +26 -22
  73. package/dist/deploy/railway/plan.d.ts +4 -5
  74. package/dist/deploy/railway/plan.js +11 -26
  75. package/dist/deploy/railway/run.d.ts +9 -9
  76. package/dist/deploy/railway/run.js +40 -40
  77. package/dist/deploy/runner.js +22 -2
  78. package/dist/deploy/secrets.d.ts +18 -9
  79. package/dist/deploy/secrets.js +64 -12
  80. package/dist/dev-supervisor.js +3 -4
  81. package/dist/engines/pi/agent-session-factory.js +20 -39
  82. package/dist/engines/pi/auth.js +43 -43
  83. package/dist/engines/pi/config.d.ts +1 -1
  84. package/dist/engines/pi/create.d.ts +14 -2
  85. package/dist/engines/pi/create.js +2 -2
  86. package/dist/engines/pi/definition.d.ts +1 -1
  87. package/dist/engines/pi/definition.js +3 -4
  88. package/dist/engines/pi/login.js +32 -16
  89. package/dist/engines/pi/open.d.ts +1 -2
  90. package/dist/engines/pi/open.js +1 -1
  91. package/dist/engines/pi/search-tools.js +1 -1
  92. package/dist/engines/pi/service.d.ts +1 -1
  93. package/dist/engines/pi/service.js +8 -0
  94. package/dist/engines/pi/session-builder.js +6 -33
  95. package/dist/engines/pi/session-control.d.ts +19 -6
  96. package/dist/engines/pi/session-control.js +565 -470
  97. package/dist/engines/pi/session-inheritance.d.ts +8 -22
  98. package/dist/engines/pi/session-inheritance.js +95 -76
  99. package/dist/engines/pi/session-markers.d.ts +48 -0
  100. package/dist/engines/pi/session-markers.js +59 -0
  101. package/dist/engines/pi/session-settings.d.ts +4 -4
  102. package/dist/engines/pi/session-settings.js +7 -4
  103. package/dist/engines/pi/session-store.d.ts +83 -13
  104. package/dist/engines/pi/session-store.js +408 -55
  105. package/dist/engines/pi/tool-context.d.ts +21 -8
  106. package/dist/engines/pi/tool-context.js +29 -4
  107. package/dist/engines/pi/tool.js +2 -2
  108. package/dist/env.js +1 -2
  109. package/dist/loader.d.ts +49 -7
  110. package/dist/loader.js +79 -18
  111. package/dist/paths.d.ts +19 -0
  112. package/dist/paths.js +27 -2
  113. package/dist/scaffold/add-channel.js +2 -2
  114. package/dist/scaffold/init.js +7 -2
  115. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  116. package/dist/schedule/discover.js +3 -15
  117. package/dist/schedule/wake-alarm.d.ts +13 -7
  118. package/dist/schedule/wake-alarm.js +88 -46
  119. package/dist/schedule/wakeups.d.ts +1 -1
  120. package/dist/schedule/wakeups.js +10 -7
  121. package/dist/service.d.ts +1 -2
  122. package/dist/service.js +27 -5
  123. package/dist/session-remote.d.ts +7 -2
  124. package/dist/session-remote.js +162 -92
  125. package/dist/session.d.ts +226 -92
  126. package/dist/session.js +61 -23
  127. package/dist/telegram.d.ts +1 -1
  128. package/dist/telegram.js +1 -1
  129. package/dist/tunnel.d.ts +18 -3
  130. package/dist/tunnel.js +32 -40
  131. package/package.json +1 -1
@@ -27,7 +27,7 @@ export async function onboardSlackInternalApp(input) {
27
27
  throw new Error("`add slack` needs an interactive terminal for internal-app creation and OAuth — " +
28
28
  "re-run in a terminal, or pass --no-onboard to scaffold only");
29
29
  }
30
- let state = await readSlackOnboardingState(input.stateRoot);
30
+ let state = readSlackOnboardingState(input.stateRoot);
31
31
  const resumed = state !== undefined;
32
32
  if (input.replaceConfig && !state) {
33
33
  throw new Error("--replace-config found no local Slack onboarding state on this machine — nothing to replace. " +
@@ -84,7 +84,7 @@ export async function onboardSlackInternalApp(input) {
84
84
  if (!configToken.startsWith("xoxe.") || !configRefreshToken.startsWith("xoxe-")) {
85
85
  throw new Error("invalid Slack configuration token prefix (expected xoxe. access + xoxe- refresh)");
86
86
  }
87
- await writeSlackOnboardingState(input.stateRoot, {
87
+ writeSlackOnboardingState(input.stateRoot, {
88
88
  ...state,
89
89
  configToken,
90
90
  configRefreshToken,
@@ -116,11 +116,11 @@ export async function onboardSlackInternalApp(input) {
116
116
  configToken,
117
117
  configRefreshToken,
118
118
  });
119
- await writeSlackOnboardingState(input.stateRoot, state);
119
+ writeSlackOnboardingState(input.stateRoot, state);
120
120
  }
121
121
  else if (input.groupBehavior.explicit && !state.appId) {
122
122
  state = { ...state, groupBehavior: input.groupBehavior.behavior };
123
- await writeSlackOnboardingState(input.stateRoot, state);
123
+ writeSlackOnboardingState(input.stateRoot, state);
124
124
  }
125
125
  if (state.createAttemptedAt && !state.appId) {
126
126
  throw new Error(`a prior Slack app creation attempt at ${state.createAttemptedAt} returned no app ID — ` +
@@ -144,6 +144,7 @@ export async function onboardSlackInternalApp(input) {
144
144
  action = answer;
145
145
  }
146
146
  if (action === "replace-config") {
147
+ console.error(`[fastagent] generate a fresh App Configuration Token pair at ${CONFIG_TOKEN_URL}`);
147
148
  openExternalUrl(CONFIG_TOKEN_URL);
148
149
  state = {
149
150
  ...state,
@@ -154,7 +155,7 @@ export async function onboardSlackInternalApp(input) {
154
155
  if (!state.configToken.startsWith("xoxe.") || !state.configRefreshToken.startsWith("xoxe-")) {
155
156
  throw new Error("invalid Slack configuration token prefix (expected xoxe. access + xoxe- refresh)");
156
157
  }
157
- await writeSlackOnboardingState(input.stateRoot, state);
158
+ writeSlackOnboardingState(input.stateRoot, state);
158
159
  }
159
160
  }
160
161
  const server = await startSlackSetupServer();
@@ -8,13 +8,9 @@ import { join, relative, resolve } from "node:path";
8
8
  import { isCancel, select } from "@clack/prompts";
9
9
  import { onboardFeishuCloudApp } from "../add-feishu.js";
10
10
  import { dotEnvPath, loadDotEnv } from "../../env.js";
11
- import { resolveStateRoot } from "../../paths.js";
12
- import { SECRETS_DIRNAME } from "../../paths.js";
11
+ import { resolveStateRoot, SECRETS_DIRNAME, isUnderDir, displayPath, exists } from "../../paths.js";
13
12
  import { detectRuntime, readPackageJson } from "../../runtime.js";
14
- import { isUnderDir } from "../../paths.js";
15
- import { displayPath } from "../../paths.js";
16
13
  import { appendChannelDotEnv, appendChannelEnv, assertChannelReady, channelExists, channelSetup, scaffoldChannel, } from "../../scaffold/add-channel.js";
17
- import { exists } from "../../paths.js";
18
14
  import { vendorSkill } from "../../scaffold/vendor-skill.js";
19
15
  import { failStartup, failUsage, placementOrExit } from "../fail.js";
20
16
  /** `fastagent add <kind> [dir]`: scaffold `channels/<kind>.ts` — the adapter import plus a starter `on()`. */
@@ -1,4 +1,4 @@
1
- import type { SessionControl, SessionEntry } from "../../session.ts";
1
+ import { type SessionControl, type SessionEntry } from "../../session.ts";
2
2
  export interface AttachOptions {
3
3
  /** Override the control endpoint (skip control.json discovery) — for a remote serve. */
4
4
  url?: string;
@@ -63,7 +63,7 @@ export interface AttachIo {
63
63
  warn: (line: string) => void;
64
64
  }
65
65
  /**
66
- * The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a `navigate`, or a
66
+ * The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a leaf move, or a
67
67
  * compaction, leaves sibling branches behind), and printing the slice raw renders the abandoned
68
68
  * branch interleaved with the live one as if it were one conversation. Reachability is computed
69
69
  * backwards from the leaf; an entry whose parent lies BEFORE the slice is path-connected by
@@ -106,5 +106,3 @@ settleMs?: number): Promise<{
106
106
  cursor: string | undefined;
107
107
  sawProgress: boolean;
108
108
  }>;
109
- /** The cursor a failed round reached, if it recorded one. */
110
- export declare function roundCursor(error: unknown): string | undefined;
@@ -16,7 +16,7 @@ import { loadDotEnv } from "../../env.js";
16
16
  import { resolveStateRoot } from "../../paths.js";
17
17
  import { log, setLogLevel } from "../../log.js";
18
18
  import { ABORTED_CODE, SESSION_BUSY_CODE } from "../../agent.js";
19
- import { NO_ACTIVE_RUN_CODE } from "../../session.js";
19
+ import { NO_ACTIVE_RUN_CODE, } from "../../session.js";
20
20
  import { ControlRequestError, connectAgent, connectSessionControl } from "../../session-remote.js";
21
21
  import { failStartup, placementOrExit } from "../fail.js";
22
22
  /** Read the serving process's local discovery file. */
@@ -157,7 +157,7 @@ export async function runAttach(sessionArg, dirArg, opts) {
157
157
  if (!endpoint)
158
158
  endpoint = discover(dir); // discovered: the first read shares the budget
159
159
  const connected = await connectSessionControl(endpoint);
160
- return { control: connected, state: await connected.state(sessionArg) };
160
+ return { control: connected, state: await connected.sessions.get(sessionArg).state() };
161
161
  }
162
162
  catch (error) {
163
163
  const decision = decideRound(errorFacts(error), {
@@ -208,7 +208,7 @@ export async function runAttach(sessionArg, dirArg, opts) {
208
208
  // ABORTED_CODE: a deliberate stop from ANY client (this attach's /abort, another attach, a
209
209
  // Web panel) — the settled line reports it either way; no source discrimination here.
210
210
  }
211
- // session_busy = the LEASE is held — by another run OR a boundary mutation (compact/set_model
211
+ // session_busy = the LEASE is held — by another run OR a boundary mutation (compact/update
212
212
  // contend on the same lease); "a run started" would be a guess, and "steer it" bad advice
213
213
  // against a compact. State the lease fact, promise nothing.
214
214
  if (sawBusy)
@@ -225,12 +225,14 @@ export async function runAttach(sessionArg, dirArg, opts) {
225
225
  void answerSlashInput(trimmed, control, (l) => console.log(l));
226
226
  return;
227
227
  }
228
- const command = trimmed === "/abort" ? { type: "abort" } : { type: "steer", prompt: { text: trimmed } };
229
- void control.dispatch(sessionArg, command).then((result) => {
228
+ const stopping = trimmed === "/abort";
229
+ const label = stopping ? "abort" : "steer";
230
+ const session = control.sessions.get(sessionArg);
231
+ void (stopping ? session.abort() : session.steer({ text: trimmed })).then((result) => {
230
232
  if (result.ok) {
231
- console.log(`[${command.type} accepted]`);
233
+ console.log(`[${label} accepted]`);
232
234
  }
233
- else if (command.type === "steer" && result.error.code === NO_ACTIVE_RUN_CODE) {
235
+ else if (!stopping && result.error.code === NO_ACTIVE_RUN_CODE) {
234
236
  // The invoke stream attach holds IS this run's driver (design: disconnect = cancel), so
235
237
  // unlike channel-started runs, this one dies with the attach. Say so up front.
236
238
  // Intent, not fact: admission may still fail (session_busy — the lease could be held
@@ -240,9 +242,9 @@ export async function runAttach(sessionArg, dirArg, opts) {
240
242
  void startRun(trimmed).catch((error) => console.log(`[prompt failed: ${String(error)}]`));
241
243
  }
242
244
  else {
243
- console.log(`[${command.type} rejected: ${result.error.code} — ${result.error.message}]`);
245
+ console.log(`[${label} rejected: ${result.error.code} — ${result.error.message}]`);
244
246
  }
245
- }, (error) => console.log(`[${command.type} failed: ${String(error)}]`));
247
+ }, (error) => console.log(`[${label} failed: ${String(error)}]`));
246
248
  });
247
249
  // Every round has ONE shape: subscribe → backfill (render the durable record since the cursor)
248
250
  // → drain live until the stream drops. Subscribing first + the server's eager registration
@@ -424,7 +426,7 @@ const isAuthError = (error) => error instanceof ControlRequestError && error.sta
424
426
  * startup (a serve restart hours in), so the local name must not imply "startup only". */
425
427
  const exitWith = failStartup;
426
428
  /**
427
- * The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a `navigate`, or a
429
+ * The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a leaf move, or a
428
430
  * compaction, leaves sibling branches behind), and printing the slice raw renders the abandoned
429
431
  * branch interleaved with the live one as if it were one conversation. Reachability is computed
430
432
  * backwards from the leaf; an entry whose parent lies BEFORE the slice is path-connected by
@@ -437,7 +439,7 @@ export function activePathSlice(entries, leafEntryId) {
437
439
  return entries;
438
440
  const byId = new Map(entries.map((e) => [e.id, e]));
439
441
  // An append always moves the leaf, so a leaf BEHIND the slice means every entry in it was
440
- // appended and then abandoned — a navigate backwards with no new turn since. Nothing here is on
442
+ // appended and then abandoned — a leaf move backwards with no new turn since. Nothing here is on
441
443
  // the active path.
442
444
  if (!byId.has(leafEntryId))
443
445
  return [];
@@ -509,7 +511,7 @@ settleMs = 300) {
509
511
  // The round HOLDS its subscription's iterator: one round = one stream, on every path — a
510
512
  // backfill failure must close it before propagating, or the caller's retry round would stack a
511
513
  // second concurrent stream interleaving the same session's output.
512
- const iterator = control.events(session)[Symbol.asyncIterator]();
514
+ const iterator = control.sessions.get(session).events()[Symbol.asyncIterator]();
513
515
  // Live output is BUFFERED while the replay block prints, then flushed — the drain runs
514
516
  // concurrently with the backfill, and interleaving raw deltas into the labeled replay would make
515
517
  // both unreadable; the [end of replay] label is only honest if the block is contiguous.
@@ -554,7 +556,7 @@ settleMs = 300) {
554
556
  let next = cursor;
555
557
  let sawBackfill = false;
556
558
  try {
557
- const backfill = await control.entries(session, cursor !== undefined ? { since: cursor } : undefined);
559
+ const backfill = await control.sessions.get(session).entries(cursor !== undefined ? { since: cursor } : undefined);
558
560
  sawBackfill = backfill.entries.length > 0;
559
561
  // Advance by APPEND ORDER (the last returned record), never by leafEntryId: `since` is an
560
562
  // append-position cursor (design §7), and a leaf that sits before later appends (abandoned
@@ -579,7 +581,7 @@ settleMs = 300) {
579
581
  }
580
582
  // The protocol's reconnect step the replay cannot cover: status changes while away are
581
583
  // LIVE-only events (state_changed before a restart is neither replayed nor re-emitted).
582
- const now = await control.state(session);
584
+ const now = await control.sessions.get(session).state();
583
585
  io.println(`[live — ${now.status}${now.activeRunId ? ` (run ${now.activeRunId})` : ""}]`);
584
586
  }
585
587
  catch (error) {
@@ -612,7 +614,7 @@ function withCursor(error, cursor) {
612
614
  return Object.assign(e, { attachCursor: cursor });
613
615
  }
614
616
  /** The cursor a failed round reached, if it recorded one. */
615
- export function roundCursor(error) {
617
+ function roundCursor(error) {
616
618
  return error.attachCursor;
617
619
  }
618
620
  /** Render one durable record on replay — the guaranteed kind vocabulary; other kinds are skipped. */
@@ -1,4 +1,10 @@
1
- export type DeployHost = "docker" | "fly" | "railway" | "agentcore";
1
+ /** The deploy targets, as a value: {@link HOST_ONLY_FLAGS} is checked against this list by a test, and
2
+ * a type alone cannot be enumerated at runtime. NOT the only copy — `cli/program.ts` repeats it as the
3
+ * `<host>` argument's `choices`, because that file may not import a command module at load time (its
4
+ * per-command imports are lazy). Adding a host means editing both; the type there rides in on a
5
+ * `import type`, which costs nothing at runtime. */
6
+ export declare const DEPLOY_HOSTS: readonly ["docker", "fly", "railway", "agentcore"];
7
+ export type DeployHost = (typeof DEPLOY_HOSTS)[number];
2
8
  export interface DeployOptions {
3
9
  run?: boolean;
4
10
  tunnel?: boolean;
@@ -12,12 +18,50 @@ export interface DeployOptions {
12
18
  /** false ⇔ `--no-input`. */
13
19
  input?: boolean;
14
20
  }
21
+ /** A flag exactly one host honours, and what the OTHERS do instead. `instead` is exhaustive over the
22
+ * non-owners, so a host added to {@link DEPLOY_HOSTS} cannot silently lose its line. */
23
+ interface HostOnlyFlag<Owner extends DeployHost> {
24
+ flag: string;
25
+ owner: Owner;
26
+ passed: (opts: DeployOptions) => boolean;
27
+ instead: Record<Exclude<DeployHost, Owner>, string>;
28
+ }
29
+ /**
30
+ * ONE table for "this flag belongs to that host", because the fact is symmetric and was not stored
31
+ * that way: each host branch stated the OTHER hosts' flags in its own words, so the same sentence
32
+ * existed three times per flag and had already drifted (`Railway-only` twice, `railway-only` once).
33
+ *
34
+ * Every row is a flag that only WARNS elsewhere. `--tunnel` is host-only too and is deliberately not
35
+ * here: it is a usage GATE (`failUsage`, exit 2) raised in `runDeploy`, not a warning — moving it in
36
+ * would downgrade a refusal to a line of advice.
37
+ *
38
+ * Exported for the exhaustiveness test: the type stops a missing `instead` line, and the test stops it
39
+ * from being typed away.
40
+ */
41
+ export declare const HOST_ONLY_FLAGS: (HostOnlyFlag<"fly"> | HostOnlyFlag<"railway">)[];
42
+ /** Exported for its own test: nothing covered these six sentences, which is how three of them came to
43
+ * disagree about the spelling of a host name. */
44
+ export declare function warnHostOnlyFlags(host: DeployHost, opts: DeployOptions): void;
15
45
  export declare function runDeploy(host: DeployHost, dirArg: string, opts: DeployOptions): Promise<void>;
16
46
  /**
17
- * Write the plan's artifacts under `target`, honouring the ownership rule: a file we did not generate is
18
- * never touched, ours is kept unless `--force`. Exported for its own test this is a four-branch state
19
- * machine over (exists, ours, force) that used to be proven by spawning the CLI eight times, which is
20
- * command LOGIC re-run through a subprocess (see vitest.config.ts) and the suite's slowest test.
47
+ * Write each generated artifact under `target`, under ONE ownership rule: **deploy only ever overwrites
48
+ * its own output.** Each generated file opens with a marker, so a file on disk is either OURS (a previous
49
+ * `deploy` wrote it) or the author's and `--force` means "my generated artifact is authoritative",
50
+ * which never licenses clobbering a file we did not write. To hand a path back to fastagent, delete it.
51
+ *
52
+ * That single rule replaced a second mechanism (a per-host list of paths exempt from `--force`), which
53
+ * only described one shape: with the agent inside the workspace, the root `.dockerignore` is the
54
+ * WORKSPACE's file and was listed; with the agent flat, the list was empty — so `--force` would have
55
+ * overwritten a hand-written root `Dockerfile` in a repository `init --flat` had explicitly adopted.
56
+ * Ownership is a property of the file, not of where the agent sits.
57
+ *
58
+ * A KEPT file of OURS that no longer matches what deploy would generate now (config/channel/lockfile/
59
+ * version drift — or the author's edits, which we cannot tell apart) is flagged stale; `--force`
60
+ * regenerates that one.
61
+ *
62
+ * Exported for its own test — this is a four-branch state machine over (exists, ours, force) that used
63
+ * to be proven by spawning the CLI eight times, which is command LOGIC re-run through a subprocess
64
+ * (see vitest.config.ts) and the suite's slowest test.
21
65
  */
22
66
  export declare function writeArtifacts(target: string, artifacts: {
23
67
  path: string;
@@ -26,3 +70,4 @@ export declare function writeArtifacts(target: string, artifacts: {
26
70
  force: boolean;
27
71
  alwaysWrite?: string[];
28
72
  }): Promise<void>;
73
+ export {};