synartesis 0.6.13 → 0.6.15

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,126 @@
2
2
 
3
3
  What changed, and why it mattered. Dates are release dates.
4
4
 
5
+ ## 0.6.15 — 2026-09-15
6
+
7
+ One gap, named by a stranger on Reddit and real.
8
+
9
+ ### Security
10
+
11
+ - **A retry of a call whose outcome nobody could establish is now held for a
12
+ person.** The tri-state was already there -- a write that times out is not
13
+ called `failed`, the resource is re-read to see which way it went, and what
14
+ cannot be settled is left `pending`, which undo refuses to step over. What
15
+ was missing was the other half: nothing stopped the agent from simply making
16
+ the call again.
17
+
18
+ The idempotency key cannot close this. It is `runId:seq`, so a retry is a new
19
+ row under a different key and no upstream can tell the two attempts were one
20
+ intention -- checked against the real proxy, which records `seq=1 key=…:1`
21
+ and `seq=2 key=…:2`. A comment beside the forward call claimed otherwise and
22
+ has been corrected.
23
+
24
+ So the person who can go and look is asked before a second side effect
25
+ exists. It reuses the gate rather than inventing a second mechanism: the call
26
+ appears in `synartesis gates` with a reason that says what happened, rather
27
+ than the generic policy text.
28
+
29
+ Three things it deliberately does not do. It never holds a **read** -- a read
30
+ that timed out is left `pending` like anything else, but reading twice
31
+ changes nothing, and an agent may always look freely. It holds only the
32
+ **identical** call, matched on server, tool and arguments: the uncertainty is
33
+ about one call, not about the tool. And approving the retry **does not
34
+ resolve the first attempt** -- that row stays `pending` and undo still stops
35
+ on it, because saying yes to going forward is not a claim about what already
36
+ happened.
37
+
38
+ ### Performance
39
+
40
+ - `findPending` runs on the way in to every write, and left to choose sqlite
41
+ walked every action in the run -- rows that carry the snapshots -- to find
42
+ the handful that are pending. A partial index over exactly those rows, named
43
+ explicitly, on a single run of twenty thousand actions with two-kilobyte
44
+ snapshots: **24ms a call to 0.004ms**.
45
+
46
+ ## 0.6.14 — 2026-09-15 (tagged, never published)
47
+
48
+ The terminal now says what to do next, and the argument parser stopped
49
+ disagreeing with itself.
50
+
51
+ ### Added
52
+
53
+ - **Most commands end by naming the one thing worth doing next**, with the
54
+ session id already filled in:
55
+
56
+ ```
57
+ fs.create_directory is held, and the agent is waiting on you: synartesis approve f0c9935d
58
+ ```
59
+
60
+ It is worked out from the journal rather than from what was typed, so it
61
+ changes as the state does. A call held for approval outranks everything else
62
+ -- that is an agent stopped mid-task waiting on a person who may not know it
63
+ is waiting. Below that: the session that changed something, what is not
64
+ pinned, and `watch`. There is at most one, there is none when nothing
65
+ applies, and `--json` never carries it. `SYNARTESIS_NO_HINTS` turns it off.
66
+
67
+ The session it names is not simply the newest one. A client that connects
68
+ and reads opens a session like any other, so "the last thing that happened"
69
+ is regularly a session in which nothing happened; a session that only read,
70
+ or was already undone, or was held and never ran, is never offered for undo.
71
+
72
+ - **A mistyped command gets the word that was meant.** `synartesis lst` used to
73
+ answer with forty lines of help, thirty-nine of them about something the
74
+ person was not doing. It now says `did you mean list?` and lists five
75
+ commands. Flags too: `--jounral`, `--dryrun`, `--forse` all resolve.
76
+
77
+ - **A flag that belongs to `proxy` is named as such.** `--http`, `--token`,
78
+ `--server` and `--log-level` are in the help page, and answering one of them
79
+ with "unknown flag" sent people hunting through that page for a flag already
80
+ in it.
81
+
82
+ ### Fixed
83
+
84
+ - **A wrapped server's own flags were read as ours.** `synartesis init db --
85
+ some-server --manifest audit.yaml` handed `--manifest` to the server
86
+ correctly and *also* took it, writing the policy to a path nobody asked for
87
+ -- on a machine where that path is real, one they already had. `positional`
88
+ and the unknown-flag check had always stopped at the bare `--`; the flag
89
+ reader had not.
90
+
91
+ - **The value of a flag was read as a flag.** `deny --reason "-see ticket 42"`
92
+ answered `unknown flag -see ticket 42`, and `prune --older-than -5` answered
93
+ `unknown flag -5` instead of the message that knows what `--older-than` is
94
+ for. Three separate lists had to agree about which flags take a value and
95
+ did not; there is one now, and a test holds it against the help page.
96
+
97
+ - **`undo --to 0` announced the session before refusing.** It printed "no
98
+ session named, so the most recent: a42bf93a" and only then rejected the
99
+ flag, which reads as though that session had been acted on.
100
+
101
+ - **A hint that could not be pasted.** `list --journal ./bench.db` named a
102
+ session, and the `show` it offered read the *default* journal and answered
103
+ "no run matches". Hints now carry `--journal` and `--manifest` when those
104
+ were not the obvious ones, and `undo --replan --dry-run` offers a command
105
+ that still replans.
106
+
107
+ - **Guessing at a typo was too eager.** A ceiling of half the word answered
108
+ `--server` with `--live` and `--token` with `--to`, neither of which anybody
109
+ meant. Two edits at most now, never for a word under three characters, and a
110
+ swapped pair counts as the one mistake it feels like -- which is what keeps
111
+ `shwo`, `pruen` and `--jounral` reachable at that ceiling.
112
+
113
+ - **Colour was decided for both streams from stdout**, so `synartesis nonsense
114
+ 2> errors.log` in a terminal wrote escape sequences into the file.
115
+
116
+ ### Performance
117
+
118
+ - **`newestUndoable` runs on every `list`** and could only be answered by
119
+ reading the action rows, which carry the snapshots -- so the cost of one
120
+ screen of sessions grew with the size of the data rather than the number of
121
+ sessions. A partial index over exactly the rows it wants fixes that, but only
122
+ when named: left to choose, sqlite takes `actions_gated` and reads the rows
123
+ anyway. Measured on a 246MB journal: **56ms to 0.01ms**.
124
+
5
125
  ## 0.6.13 — 2026-09-14
6
126
 
7
127
  Four things, all of them from one comment by a stranger on Reddit who had
@@ -10,6 +130,26 @@ would have announced itself.
10
130
 
11
131
  ### Security
12
132
 
133
+ - **A server that would not start said nothing useful about why, and could
134
+ hang the proxy outright.** Two faults in the same few lines, found by hitting
135
+ the first one in real use.
136
+
137
+ What it printed was `code: 'MODULE_NOT_FOUND', requireStack: [], },
138
+ Node.js v22.16.0` -- the last four lines of node's output, which are the four
139
+ least informative. `Error: Cannot find module '/path/to/thing'` sits in the
140
+ middle and was thrown away, so the message named neither the file nor
141
+ anything to act on. A line that states a fault now leads, stack frames are
142
+ dropped, and the tail is only the fallback when nothing states one.
143
+
144
+ The second was worse and was found while testing the first: nothing read the
145
+ server's stderr until after the connection failed. A pipe nobody reads fills
146
+ at around 64kB and blocks the writer, so a server that said more than that
147
+ before dying never exited, the connect never rejected, and `check` sat there
148
+ for ever with no output at all. Measured against a server writing 2.4MB to
149
+ stderr: **killed at 30 seconds, against 0.8 seconds and the right error**.
150
+ Stderr is now read as it arrives, capped so a logging loop cannot make the
151
+ proxy the thing that runs out of memory.
152
+
13
153
  - **The one remaining advisory is gone.** `esbuild` reached `pnpm audit`
14
154
  through vite, in the renderer build only -- never in anything published, and
15
155
  `--prod` was already clean. Pinned anyway, the same way as 0.6.12's three:
@@ -6,6 +6,19 @@ import {
6
6
  describe
7
7
  } from "./chunk-YVOO3PTV.js";
8
8
 
9
+ // src/proxy/flags.ts
10
+ var PROXY_FLAGS = [
11
+ "--manifest",
12
+ "--journal",
13
+ "--server",
14
+ "--gate-timeout",
15
+ "--log-level",
16
+ "--http",
17
+ "--http-host",
18
+ "--http-idle",
19
+ "--token"
20
+ ];
21
+
9
22
  // src/invocation.ts
10
23
  import { spawnSync } from "child_process";
11
24
  import { accessSync, constants } from "fs";
@@ -131,23 +144,25 @@ var INK = `${ESC}38;2;214;201;197m`;
131
144
  var DIM = `${ESC}2m`;
132
145
  var BOLD = `${ESC}1m`;
133
146
  var RESET = `${ESC}0m`;
134
- var enabled = process.env["NO_COLOR"] === void 0 && process.env["TERM"] !== "dumb" && process.stdout.isTTY;
135
- function paint(codes, text) {
136
- return enabled ? `${codes}${text}${RESET}` : text;
147
+ function usable(stream) {
148
+ return process.env["NO_COLOR"] === void 0 && process.env["TERM"] !== "dumb" && stream.isTTY;
137
149
  }
138
150
  function spaced(text) {
139
151
  return Array.from(text).join(" ");
140
152
  }
141
- var style = {
142
- /** A section label: small, capital, spaced out. */
143
- label: (text) => paint(ACCENT + DIM, spaced(text.toUpperCase())),
144
- heading: (text) => paint(INK, text.toUpperCase()),
145
- accent: (text) => paint(ACCENT, text),
146
- strong: (text) => paint(INK, text),
147
- quiet: (text) => paint(DIM, text),
148
- /** Off-white on oxblood, the way the wordmark is set. */
149
- plate: (text) => paint(ON_ACCENT + BOLD, ` ${text} `)
150
- };
153
+ function palette(on) {
154
+ const paint = (codes, text) => on ? `${codes}${text}${RESET}` : text;
155
+ return {
156
+ label: (text) => paint(ACCENT + DIM, spaced(text.toUpperCase())),
157
+ heading: (text) => paint(INK, text.toUpperCase()),
158
+ accent: (text) => paint(ACCENT, text),
159
+ strong: (text) => paint(INK, text),
160
+ quiet: (text) => paint(DIM, text),
161
+ plate: (text) => paint(ON_ACCENT + BOLD, ` ${text} `)
162
+ };
163
+ }
164
+ var style = palette(usable(process.stdout));
165
+ var errorStyle = palette(usable(process.stderr));
151
166
  var WORDMARK = spaced("SYNARTESIS");
152
167
  function meander(width) {
153
168
  const unit = "\u2517\u2501\u2513\u250F\u2501\u251B";
@@ -270,6 +285,34 @@ CREATE INDEX IF NOT EXISTS actions_gated ON actions(status, ts);
270
285
  -- no meaning changes, IF NOT EXISTS makes it idempotent, and an older build
271
286
  -- opening the same file afterwards neither notices nor cares.
272
287
  CREATE INDEX IF NOT EXISTS actions_run_status ON actions(run_id, status);
288
+
289
+ -- Partial, which is what makes it small: newestUndoable asks for the newest run
290
+ -- holding something an undo would reverse, and that is a handful of rows out of
291
+ -- a table where every other row is a read, a refusal or something already put
292
+ -- back. Without it the question can only be answered by reaching into the table
293
+ -- to test inverse_json on every applied row -- and those rows carry the
294
+ -- snapshots, so the cost of the hint at the foot of a session list grew with
295
+ -- the size of the data rather than with the number of sessions, which is
296
+ -- exactly what the index above was added to stop. Measured on sixty runs of a
297
+ -- thousand actions with two-kilobyte snapshots, a 246MB journal: 56ms to
298
+ -- 0.01ms.
299
+ --
300
+ -- newestUndoable names this index with INDEXED BY, and has to: left to choose,
301
+ -- sqlite takes actions_gated for the status seek and then reads the rows. See
302
+ -- the query for why that is preferred to running ANALYZE.
303
+ --
304
+ -- Added the same way as the three above and for the same reason: no row
305
+ -- changes, no meaning changes, IF NOT EXISTS makes it idempotent, and an older
306
+ -- build opening the same file afterwards neither notices nor cares.
307
+ CREATE INDEX IF NOT EXISTS actions_undoable ON actions(run_id)
308
+ WHERE status = 'applied' AND inverse_json IS NOT NULL;
309
+
310
+ -- Partial for the same reason: findPending runs on the way in to every write,
311
+ -- looking for an earlier attempt at that exact call whose outcome was never
312
+ -- established. Those are rare, so the index holds almost nothing -- while the
313
+ -- rows it saves reading are the ones carrying the snapshots.
314
+ CREATE INDEX IF NOT EXISTS actions_unresolved ON actions(run_id, server, tool)
315
+ WHERE status = 'pending';
273
316
  `;
274
317
 
275
318
  // src/journal/journal.ts
@@ -681,6 +724,17 @@ var SqliteJournal = class {
681
724
  return rows.find((row) => canonical(row.args) === wanted);
682
725
  });
683
726
  }
727
+ findPending(query) {
728
+ return this.#run("findPending", () => {
729
+ const rows = this.#db.prepare(
730
+ `SELECT * FROM actions INDEXED BY actions_unresolved
731
+ WHERE run_id = ? AND server = ? AND tool = ? AND status = 'pending'
732
+ ORDER BY seq`
733
+ ).all(query.runId, query.server, query.tool).map(toAction);
734
+ const wanted = canonical(query.args ?? {});
735
+ return rows.find((row) => canonical(row.args) === wanted);
736
+ });
737
+ }
684
738
  getAction(actionId) {
685
739
  return this.#run("getAction", () => {
686
740
  const raw = this.#db.prepare("SELECT * FROM actions WHERE id = ?").get(actionId);
@@ -743,6 +797,33 @@ var SqliteJournal = class {
743
797
  return tally;
744
798
  });
745
799
  }
800
+ /**
801
+ * The newest run that still holds something an undo would put back.
802
+ *
803
+ * Not simply the newest run, and not the newest run with an applied action
804
+ * either. A client that connects and reads opens a session like any other,
805
+ * and reads are recorded and left `applied` -- rollback steps past them --
806
+ * so `applied` alone regularly points at a session in which nothing
807
+ * happened. What makes a run worth offering is an action that is still
808
+ * applied and carries the inverse that would reverse it, which is the same
809
+ * test `undo` itself uses when it goes looking for the session somebody
810
+ * meant. That inverse is also what excludes the reads: the manifest loader
811
+ * refuses an inverse on a readonly tool, so a read can never carry one, and
812
+ * a second condition on the class would be a condition that cannot fire.
813
+ */
814
+ newestUndoable() {
815
+ return this.#run("newestUndoable", () => {
816
+ const raw = this.#db.prepare(
817
+ `SELECT * FROM runs
818
+ WHERE id IN (SELECT run_id FROM actions INDEXED BY actions_undoable
819
+ WHERE status = 'applied'
820
+ AND inverse_json IS NOT NULL)
821
+ ORDER BY started_at DESC, rowid DESC
822
+ LIMIT 1`
823
+ ).get();
824
+ return raw === void 0 ? void 0 : toRun(raw);
825
+ });
826
+ }
746
827
  recentActions(limit) {
747
828
  return this.#run(
748
829
  "recentActions",
@@ -1526,6 +1607,25 @@ function describeError(error) {
1526
1607
  return error instanceof Error ? error.message : String(error);
1527
1608
  }
1528
1609
  var PROXY_CLIENT_INFO = { name: "synartesis-proxy", version: "0.0.0" };
1610
+ var STDERR_KEPT = 256 * 1024;
1611
+ var STDERR_SETTLE_MS = 200;
1612
+ function settled(stream, ms) {
1613
+ if (stream.readableEnded === true) {
1614
+ return Promise.resolve();
1615
+ }
1616
+ return new Promise((resolve2) => {
1617
+ const done = () => {
1618
+ clearTimeout(timer);
1619
+ resolve2();
1620
+ };
1621
+ const timer = setTimeout(done, ms);
1622
+ timer.unref?.();
1623
+ stream.on("end", done);
1624
+ });
1625
+ }
1626
+ function isReadable(stream) {
1627
+ return typeof stream === "object" && stream !== null && "on" in stream && typeof stream.on === "function";
1628
+ }
1529
1629
  function bufferedText(stream) {
1530
1630
  if (typeof stream !== "object" || stream === null || !("read" in stream)) {
1531
1631
  return "";
@@ -1534,14 +1634,26 @@ function bufferedText(stream) {
1534
1634
  if (typeof read2 !== "function") {
1535
1635
  return "";
1536
1636
  }
1537
- const chunk = read2.call(stream);
1538
- if (typeof chunk === "string") {
1539
- return chunk;
1637
+ let text = "";
1638
+ for (; ; ) {
1639
+ const chunk = read2.call(stream);
1640
+ if (typeof chunk === "string") {
1641
+ text += chunk;
1642
+ } else if (Buffer.isBuffer(chunk)) {
1643
+ text += chunk.toString("utf8");
1644
+ } else {
1645
+ break;
1646
+ }
1540
1647
  }
1541
- return Buffer.isBuffer(chunk) ? chunk.toString("utf8") : "";
1648
+ return text;
1542
1649
  }
1650
+ var NAMES_A_FAULT = /^(?:[A-Za-z]*(?:Error|Exception)\b|Cannot find |ENOENT\b|EACCES\b|EADDRINUSE\b|.*: command not found)/;
1543
1651
  function lastWords(text) {
1544
- const lines = text.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
1652
+ const lines = text.split("\n").map((line) => line.trim()).filter((line) => line !== "" && !/^at\s/.test(line));
1653
+ const named = lines.find((line) => NAMES_A_FAULT.test(line));
1654
+ if (named !== void 0) {
1655
+ return named;
1656
+ }
1545
1657
  const kept = lines.slice(-4).join("; ");
1546
1658
  return kept === "" ? void 0 : kept;
1547
1659
  }
@@ -1573,11 +1685,22 @@ async function start(spec) {
1573
1685
  });
1574
1686
  const client = new Client({ ...PROXY_CLIENT_INFO });
1575
1687
  let said = "";
1688
+ const stderr = transport.stderr;
1689
+ const listening = isReadable(stderr);
1690
+ if (listening) {
1691
+ stderr.on("data", (chunk) => {
1692
+ if (chunk !== void 0 && said.length < STDERR_KEPT) {
1693
+ said += typeof chunk === "string" ? chunk : chunk.toString("utf8");
1694
+ }
1695
+ });
1696
+ }
1576
1697
  try {
1577
1698
  await client.connect(transport);
1578
1699
  } catch (error) {
1579
- said = bufferedText(transport.stderr);
1580
- const reason = lastWords(said);
1700
+ if (listening) {
1701
+ await settled(stderr, STDERR_SETTLE_MS);
1702
+ }
1703
+ const reason = lastWords(said === "" ? bufferedText(transport.stderr) : said);
1581
1704
  throw new UpstreamError(
1582
1705
  spec.name,
1583
1706
  "connect",
@@ -1741,6 +1864,7 @@ async function observeState(router, read2, signal) {
1741
1864
  }
1742
1865
 
1743
1866
  export {
1867
+ PROXY_FLAGS,
1744
1868
  cliCommand,
1745
1869
  cliCommandFrom,
1746
1870
  proxyCommand,
@@ -1748,6 +1872,7 @@ export {
1748
1872
  findManifest,
1749
1873
  findJournal,
1750
1874
  style,
1875
+ errorStyle,
1751
1876
  WORDMARK,
1752
1877
  rule,
1753
1878
  mark,
@@ -1782,4 +1907,4 @@ export {
1782
1907
  observeState,
1783
1908
  connectStdioUpstream
1784
1909
  };
1785
- //# sourceMappingURL=chunk-6X7GPUGJ.js.map
1910
+ //# sourceMappingURL=chunk-OODDXM36.js.map
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  IDEMPOTENCY_META_KEY,
4
4
  NOTHING_RECORDED_YET,
5
+ PROXY_FLAGS,
5
6
  WORDMARK,
6
7
  banner,
7
8
  canonical,
@@ -10,6 +11,7 @@ import {
10
11
  createPolicyResolver,
11
12
  createRouter,
12
13
  describeStanding,
14
+ errorStyle,
13
15
  findJournal,
14
16
  findManifest,
15
17
  labelFor,
@@ -34,7 +36,7 @@ import {
34
36
  verifyAgainstServers,
35
37
  warnUntested,
36
38
  wasRefused
37
- } from "./chunk-6X7GPUGJ.js";
39
+ } from "./chunk-OODDXM36.js";
38
40
  import {
39
41
  DriftConflict,
40
42
  ManifestError,
@@ -786,24 +788,6 @@ function verdict(inspection) {
786
788
  return "nothing was recorded in this session";
787
789
  }
788
790
 
789
- // src/watch.ts
790
- import { existsSync as existsSync2 } from "fs";
791
-
792
- // src/keys.ts
793
- var SEQUENCE = /^\u001b(\[[0-9;?]*[ -\/]*[@-~]|O[@-~])/;
794
- function keysIn(chunk) {
795
- const keys = [];
796
- let at = 0;
797
- while (at < chunk.length) {
798
- const rest = chunk.slice(at);
799
- const sequence = rest.startsWith("\x1B") ? SEQUENCE.exec(rest) : null;
800
- const key = sequence?.[0] ?? rest.slice(0, 1);
801
- keys.push(key);
802
- at += key.length;
803
- }
804
- return keys;
805
- }
806
-
807
791
  // src/clock.ts
808
792
  var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
809
793
  var pad = (value) => String(value).padStart(2, "0");
@@ -855,6 +839,169 @@ function ago(iso, now = /* @__PURE__ */ new Date()) {
855
839
  return `${String(Math.round(hours / 24))}d ago`;
856
840
  }
857
841
 
842
+ // src/hints.ts
843
+ function hintsWanted() {
844
+ return process.env["SYNARTESIS_NO_HINTS"] === void 0;
845
+ }
846
+ function hintLine(hint2, paths = "") {
847
+ if (hint2.run === void 0) {
848
+ return ` ${style.quiet(hint2.why)}`;
849
+ }
850
+ return ` ${style.quiet(`${hint2.why}:`)} ${style.strong(`${cliCommand()} ${hint2.run}${paths}`)}`;
851
+ }
852
+ function short(id) {
853
+ return id.slice(0, 8);
854
+ }
855
+ function heldCalls(journal) {
856
+ const waiting = journal.listGated();
857
+ const [first, ...rest] = waiting;
858
+ if (first === void 0) {
859
+ return void 0;
860
+ }
861
+ if (rest.length === 0) {
862
+ return {
863
+ why: `${first.server}.${first.tool} is held, and the agent is waiting on you`,
864
+ run: `approve ${short(first.id)}`,
865
+ needs: ["journal"]
866
+ };
867
+ }
868
+ return {
869
+ why: `${String(waiting.length)} calls are held, and the agent is waiting on you`,
870
+ run: "approve --all",
871
+ needs: ["journal"]
872
+ };
873
+ }
874
+ function whatChanged(journal) {
875
+ const run = journal.newestUndoable();
876
+ if (run === void 0) {
877
+ return void 0;
878
+ }
879
+ return {
880
+ why: `${run.label ?? "an agent"} changed something ${ago(run.startedAt)}`,
881
+ run: `show ${short(run.id)}`,
882
+ needs: ["journal"]
883
+ };
884
+ }
885
+ function notPinned(manifest) {
886
+ const pinned = Object.keys(manifest.pins ?? {});
887
+ const loose = Object.keys(manifest.servers).filter((name) => !pinned.includes(name));
888
+ if (loose.length === 0) {
889
+ return void 0;
890
+ }
891
+ return {
892
+ why: pinned.length === 0 ? "no tool here is pinned, so a server that changes shape keeps its old policy" : `${loose.join(", ")} ${loose.length === 1 ? "is" : "are"} not pinned`,
893
+ run: "pin",
894
+ needs: ["manifest"]
895
+ };
896
+ }
897
+ var LEAVE_IT_RUNNING = {
898
+ why: "anything an agent holds will appear here as it happens",
899
+ run: "watch",
900
+ needs: ["journal"]
901
+ };
902
+ function afterStatus(journal) {
903
+ if (journal === void 0) {
904
+ return LEAVE_IT_RUNNING;
905
+ }
906
+ return firstOf(
907
+ () => heldCalls(journal),
908
+ () => whatChanged(journal),
909
+ () => LEAVE_IT_RUNNING
910
+ );
911
+ }
912
+ function firstOf(...candidates2) {
913
+ for (const candidate of candidates2) {
914
+ const hint2 = candidate();
915
+ if (hint2 !== void 0) {
916
+ return hint2;
917
+ }
918
+ }
919
+ return void 0;
920
+ }
921
+ function didYouMean(typed, known) {
922
+ if (typed.length < 3) {
923
+ return void 0;
924
+ }
925
+ const ceiling = typed.length <= 4 ? 1 : 2;
926
+ let best;
927
+ let bestAt = ceiling + 1;
928
+ for (const candidate of known) {
929
+ const gap = distance(typed, candidate);
930
+ if (gap < bestAt) {
931
+ best = candidate;
932
+ bestAt = gap;
933
+ }
934
+ }
935
+ return bestAt <= ceiling ? best : void 0;
936
+ }
937
+ function distance(from, to) {
938
+ const rows = [];
939
+ for (let i = 0; i <= from.length; i += 1) {
940
+ const row = new Array(to.length + 1).fill(0);
941
+ row[0] = i;
942
+ rows.push(row);
943
+ }
944
+ const top = rows[0];
945
+ for (let j = 0; j <= to.length && top !== void 0; j += 1) {
946
+ top[j] = j;
947
+ }
948
+ for (let i = 1; i <= from.length; i += 1) {
949
+ for (let j = 1; j <= to.length; j += 1) {
950
+ const same = from[i - 1] === to[j - 1];
951
+ let best = Math.min(
952
+ (rows[i - 1]?.[j] ?? 0) + 1,
953
+ (rows[i]?.[j - 1] ?? 0) + 1,
954
+ (rows[i - 1]?.[j - 1] ?? 0) + (same ? 0 : 1)
955
+ );
956
+ if (i > 1 && j > 1 && from[i - 1] === to[j - 2] && from[i - 2] === to[j - 1]) {
957
+ best = Math.min(best, (rows[i - 2]?.[j - 2] ?? 0) + 1);
958
+ }
959
+ const row = rows[i];
960
+ if (row !== void 0) {
961
+ row[j] = best;
962
+ }
963
+ }
964
+ }
965
+ return rows[from.length]?.[to.length] ?? to.length;
966
+ }
967
+ var EVERYDAY = [
968
+ ["install", "cover the servers your client already lists"],
969
+ ["watch", "see what an agent does, and answer what it holds"],
970
+ ["list", "the sessions recorded so far"],
971
+ ["show", "what one session did, and whether it still holds"],
972
+ ["undo", "put back what one of them changed"]
973
+ ];
974
+ function shortList() {
975
+ const self = cliCommand();
976
+ return [
977
+ "",
978
+ ...EVERYDAY.map(
979
+ ([name, said]) => ` ${errorStyle.strong(`${self} ${name.padEnd(8)}`)} ${errorStyle.quiet(said)}`
980
+ ),
981
+ "",
982
+ ` ${errorStyle.quiet(`${self} --help for every command.`)}`,
983
+ ""
984
+ ].join("\n");
985
+ }
986
+
987
+ // src/watch.ts
988
+ import { existsSync as existsSync2 } from "fs";
989
+
990
+ // src/keys.ts
991
+ var SEQUENCE = /^\u001b(\[[0-9;?]*[ -\/]*[@-~]|O[@-~])/;
992
+ function keysIn(chunk) {
993
+ const keys = [];
994
+ let at = 0;
995
+ while (at < chunk.length) {
996
+ const rest = chunk.slice(at);
997
+ const sequence = rest.startsWith("\x1B") ? SEQUENCE.exec(rest) : null;
998
+ const key = sequence?.[0] ?? rest.slice(0, 1);
999
+ keys.push(key);
1000
+ at += key.length;
1001
+ }
1002
+ return keys;
1003
+ }
1004
+
858
1005
  // src/describe.ts
859
1006
  import { basename } from "path";
860
1007
  var SUBJECT_KEYS = [
@@ -1513,8 +1660,8 @@ function pruneBackups(path) {
1513
1660
  try {
1514
1661
  const dir = dirname(path);
1515
1662
  const prefix = `${basename2(path)}.synartesis-backup-`;
1516
- const ours = readdirSync(dir).filter((name) => name.startsWith(prefix)).sort();
1517
- for (const name of ours.slice(0, Math.max(0, ours.length - KEEP_BACKUPS))) {
1663
+ const ours2 = readdirSync(dir).filter((name) => name.startsWith(prefix)).sort();
1664
+ for (const name of ours2.slice(0, Math.max(0, ours2.length - KEEP_BACKUPS))) {
1518
1665
  rmSync(join(dir, name), { force: true });
1519
1666
  }
1520
1667
  } catch {
@@ -2668,6 +2815,10 @@ it and is found from any directory inside it, the way a version control tool
2668
2815
  finds its root; anything else lives in ~/.synartesis, which is where the
2669
2816
  journal is too. Set SYNARTESIS_HOME to put that somewhere else.
2670
2817
 
2818
+ Most commands end by naming the one thing worth doing next, worked out from
2819
+ what is actually in the journal rather than from what was typed. Set
2820
+ SYNARTESIS_NO_HINTS to turn that off; --json never carries it.
2821
+
2671
2822
  Exit codes: 0 complete, 1 halted or partial, 2 bad usage or configuration.
2672
2823
  `;
2673
2824
  var UsageError = class extends Error {
@@ -2685,25 +2836,37 @@ var UsageError = class extends Error {
2685
2836
  this.listCommands = listCommands;
2686
2837
  }
2687
2838
  };
2839
+ function ours(argv) {
2840
+ const end = argv.indexOf("--");
2841
+ return end === -1 ? argv : argv.slice(0, end);
2842
+ }
2688
2843
  function flag(argv, name) {
2689
- const at = argv.indexOf(name);
2844
+ const mine = ours(argv);
2845
+ const at = mine.indexOf(name);
2690
2846
  if (at === -1) {
2691
2847
  return void 0;
2692
2848
  }
2693
- const value = argv[at + 1];
2849
+ const value = mine[at + 1];
2694
2850
  if (value === void 0 || value.startsWith("--")) {
2695
2851
  throw new UsageError(`${name} needs a value`);
2696
2852
  }
2697
2853
  return value;
2698
2854
  }
2855
+ var TAKES_VALUE = /* @__PURE__ */ new Set([
2856
+ "--manifest",
2857
+ "--journal",
2858
+ "--to",
2859
+ "--by",
2860
+ "--reason",
2861
+ "--older-than",
2862
+ "--client"
2863
+ ]);
2699
2864
  function positional(argv) {
2700
- const skip = /* @__PURE__ */ new Set(["--manifest", "--journal", "--to", "--by", "--reason", "--gate-timeout", "--older-than", "--client"]);
2701
2865
  const values = [];
2702
- const end = argv.indexOf("--");
2703
- const ours = end === -1 ? argv : argv.slice(0, end);
2704
- for (let i = 0; i < ours.length; i += 1) {
2705
- const token = ours[i] ?? "";
2706
- if (skip.has(token)) {
2866
+ const mine = ours(argv);
2867
+ for (let i = 0; i < mine.length; i += 1) {
2868
+ const token = mine[i] ?? "";
2869
+ if (TAKES_VALUE.has(token)) {
2707
2870
  i += 1;
2708
2871
  continue;
2709
2872
  }
@@ -2804,6 +2967,16 @@ async function runCheck(argv) {
2804
2967
  out("");
2805
2968
  out(` ${style.quiet("Anything not mentioned here is treated as irreversible and guarded.")}`);
2806
2969
  out("");
2970
+ hint(
2971
+ firstOf(
2972
+ () => notPinned(manifest),
2973
+ () => ({
2974
+ why: "this is sound; to see which clients it covers",
2975
+ run: "status",
2976
+ needs: ["manifest", "journal"]
2977
+ })
2978
+ )
2979
+ );
2807
2980
  return 0;
2808
2981
  }
2809
2982
  async function runInstall(argv) {
@@ -2997,6 +3170,9 @@ function runStatus(argv) {
2997
3170
  waiting === 0 ? ` ${style.quiet("Everything found is covered.")}` : ` ${style.accent(`${String(waiting)} not covered.`)} ${style.quiet(`${cliCommand()} install covers them.`)}`
2998
3171
  );
2999
3172
  out("");
3173
+ if (waiting === 0) {
3174
+ hint(afterStatus(journal));
3175
+ }
3000
3176
  } finally {
3001
3177
  journal?.close();
3002
3178
  }
@@ -3096,6 +3272,13 @@ function out(line2) {
3096
3272
  process.stdout.write(`${line2}
3097
3273
  `);
3098
3274
  }
3275
+ function hint(candidate) {
3276
+ if (candidate === void 0 || !hintsWanted()) {
3277
+ return;
3278
+ }
3279
+ out(hintLine(candidate, pathArgs(candidate.needs)));
3280
+ out("");
3281
+ }
3099
3282
  function runList(journal, asJson, journalPath) {
3100
3283
  const runs = [...journal.listRuns()].reverse();
3101
3284
  const tally2 = journal.tallyRuns();
@@ -3134,6 +3317,10 @@ function runList(journal, asJson, journalPath) {
3134
3317
  out(` ${style.quiet(`This journal is ${sizeOf(journalPath)}; synartesis prune reclaims what is old enough to lose.`)}`);
3135
3318
  out("");
3136
3319
  }
3320
+ hint(firstOf(
3321
+ () => heldCalls(journal),
3322
+ () => whatChanged(journal)
3323
+ ));
3137
3324
  return 0;
3138
3325
  }
3139
3326
  async function runShow(argv, journal, asJson) {
@@ -3231,6 +3418,16 @@ async function runShow(argv, journal, asJson) {
3231
3418
  out(
3232
3419
  ` ${style.quiet("undo stops at the first of them; ")}${style.strong(`${cliCommand()} undo ${runId.slice(0, 8)} --force`)}${style.quiet(" shows what it would write")}`
3233
3420
  );
3421
+ } else if (actions.some((action) => action.status === "applied" && action.inverse !== void 0)) {
3422
+ out("");
3423
+ hint({
3424
+ // Not a restatement of the verdict directly above it, which has just
3425
+ // said in its own words that nothing has moved. A hint that repeats
3426
+ // the line above is a line nobody reads twice.
3427
+ why: "to see the plan, before anything is written",
3428
+ run: `undo ${runId.slice(0, 8)} --dry-run`,
3429
+ needs: ["manifest", "journal"]
3430
+ });
3234
3431
  }
3235
3432
  } else {
3236
3433
  out("");
@@ -3320,6 +3517,13 @@ function summarise2(actions) {
3320
3517
  return `${String(actions.length)} actions: ${parts.join(", ")} | ${String(undoable)} with a recorded undo`;
3321
3518
  }
3322
3519
  var journalArg = "";
3520
+ var manifestArg = "";
3521
+ function pathArgs(needs) {
3522
+ if (needs === void 0) {
3523
+ return "";
3524
+ }
3525
+ return needs.map((what) => what === "journal" ? journalArg : manifestArg).join("");
3526
+ }
3323
3527
  var PRUNE_NAG_BYTES = 100 * 1024 * 1024;
3324
3528
  function bytesOf(path) {
3325
3529
  try {
@@ -3398,6 +3602,10 @@ function runClose(argv, journal) {
3398
3602
  out("");
3399
3603
  out(` ${style.quiet("nothing is open; every run has ended cleanly")}`);
3400
3604
  out("");
3605
+ hint(firstOf(
3606
+ () => heldCalls(journal),
3607
+ () => whatChanged(journal)
3608
+ ));
3401
3609
  return 0;
3402
3610
  }
3403
3611
  const run = given === void 0 ? pick(active, void 0, RUN, true) : pick([...all].reverse(), given, RUN, true);
@@ -3419,6 +3627,10 @@ function runGates(journal, asJson) {
3419
3627
  out("");
3420
3628
  out(` ${style.quiet("Nothing is waiting for a decision.")}`);
3421
3629
  out("");
3630
+ hint(firstOf(
3631
+ () => whatChanged(journal),
3632
+ () => LEAVE_IT_RUNNING
3633
+ ));
3422
3634
  return 0;
3423
3635
  }
3424
3636
  out("");
@@ -3447,10 +3659,10 @@ function runDecision(argv, journal, approving) {
3447
3659
  const by = flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown";
3448
3660
  const reason = flag(argv, "--reason") ?? "denied by operator";
3449
3661
  if (given !== void 0) {
3450
- const settled = journal.getAction(given);
3451
- if (settled !== void 0 && settled.status !== "gated") {
3662
+ const settled2 = journal.getAction(given);
3663
+ if (settled2 !== void 0 && settled2.status !== "gated") {
3452
3664
  process.stderr.write(
3453
- `synartesis: ${given} is no longer awaiting approval (it is ${labelFor(settled)})
3665
+ `synartesis: ${given} is no longer awaiting approval (it is ${labelFor(settled2)})
3454
3666
  `
3455
3667
  );
3456
3668
  return 1;
@@ -3462,6 +3674,7 @@ function runDecision(argv, journal, approving) {
3462
3674
  return 0;
3463
3675
  }
3464
3676
  let failed = 0;
3677
+ let settled = 0;
3465
3678
  for (const action of targets) {
3466
3679
  const changed = approving ? journal.approve(action.id, by) : journal.deny(action.id, by, reason);
3467
3680
  if (!changed) {
@@ -3473,13 +3686,23 @@ function runDecision(argv, journal, approving) {
3473
3686
  failed += 1;
3474
3687
  continue;
3475
3688
  }
3689
+ settled += 1;
3476
3690
  out(
3477
3691
  ` ${style.accent(approving ? "approved" : "denied")} ${style.strong(`${action.server}.${action.tool}`)} ${style.quiet(action.id)}`
3478
3692
  );
3479
3693
  }
3694
+ if (settled > 0) {
3695
+ out("");
3696
+ hint(
3697
+ firstOf(
3698
+ () => heldCalls(journal),
3699
+ () => approving ? { why: "the agent can make that call again now, and it will go through" } : { why: "the call was refused; the agent is told, and decides what to do next" }
3700
+ )
3701
+ );
3702
+ }
3480
3703
  return failed === 0 ? 0 : 1;
3481
3704
  }
3482
- function report(result, alreadyForcing = false) {
3705
+ function report(result, alreadyForcing = false, as = "") {
3483
3706
  out("");
3484
3707
  out(` ${style.label(result.dryRun ? "dry run" : "undo")} ${style.strong(result.runId)}`);
3485
3708
  out(` ${rule(72)}`);
@@ -3552,6 +3775,13 @@ function report(result, alreadyForcing = false) {
3552
3775
  ` ${style.label("result")} ${result.status === "rolled_back" ? result.status : style.accent(result.status)}`
3553
3776
  );
3554
3777
  out("");
3778
+ if (result.dryRun && result.status === "rolled_back") {
3779
+ hint({
3780
+ why: "nothing was written; to do exactly this for real",
3781
+ run: `undo ${result.runId.slice(0, 8)}${as}`,
3782
+ needs: ["manifest", "journal"]
3783
+ });
3784
+ }
3555
3785
  return result.status === "rolled_back" ? 0 : 1;
3556
3786
  }
3557
3787
  async function withUpstreams(manifestPath, use, only) {
@@ -3612,6 +3842,11 @@ async function performUndo(manifestPath, journal, runId, options) {
3612
3842
  );
3613
3843
  }
3614
3844
  async function runUndo(argv, journal) {
3845
+ const rawTo = flag(argv, "--to");
3846
+ const toSeq = rawTo === void 0 ? void 0 : Number(rawTo);
3847
+ if (toSeq !== void 0 && (!Number.isInteger(toSeq) || toSeq < 1)) {
3848
+ throw new UsageError("--to needs a positive whole number");
3849
+ }
3615
3850
  const given = positional(argv)[1];
3616
3851
  const chosen = pick([...journal.listRuns()].reverse(), given, RUN, true);
3617
3852
  const runId = chosen.id;
@@ -3647,11 +3882,6 @@ async function runUndo(argv, journal) {
3647
3882
  return 0;
3648
3883
  }
3649
3884
  }
3650
- const rawTo = flag(argv, "--to");
3651
- const toSeq = rawTo === void 0 ? void 0 : Number(rawTo);
3652
- if (toSeq !== void 0 && (!Number.isInteger(toSeq) || toSeq < 1)) {
3653
- throw new UsageError("--to needs a positive whole number");
3654
- }
3655
3885
  if (toSeq !== void 0) {
3656
3886
  const highest = journal.getActions(runId).reduce((top, action) => Math.max(top, action.seq), 0);
3657
3887
  if (toSeq > highest) {
@@ -3695,16 +3925,42 @@ async function runUndo(argv, journal) {
3695
3925
  return 1;
3696
3926
  }
3697
3927
  }
3928
+ const replan = argv.includes("--replan");
3698
3929
  return report(
3699
3930
  await performUndo(manifestPath, journal, runId, {
3700
3931
  dryRun: argv.includes("--dry-run"),
3701
3932
  ...toSeq === void 0 ? {} : { toSeq },
3702
- replan: argv.includes("--replan"),
3933
+ replan,
3703
3934
  ...forcing && said ? { force: true } : {}
3704
3935
  }),
3705
- forcing
3936
+ forcing,
3937
+ // --to is deliberately absent, and cannot reach here: a floor leaves
3938
+ // actions below it alone, which makes the result `partial`, and the hint
3939
+ // is only offered on `rolled_back`.
3940
+ `${replan ? " --replan" : ""}${forcing && said ? " --force --yes" : ""}`
3706
3941
  );
3707
3942
  }
3943
+ var KNOWN_COMMANDS = [
3944
+ "install",
3945
+ "uninstall",
3946
+ "status",
3947
+ "init",
3948
+ "check",
3949
+ "pin",
3950
+ "list",
3951
+ "show",
3952
+ "gates",
3953
+ "close",
3954
+ "prune",
3955
+ "proxy",
3956
+ "desktop",
3957
+ "watch",
3958
+ "approve",
3959
+ "deny",
3960
+ "undo",
3961
+ "help",
3962
+ "version"
3963
+ ];
3708
3964
  var FLAGS = /* @__PURE__ */ new Set([
3709
3965
  // Two lists have to agree about a flag: this one decides whether it is
3710
3966
  // accepted at all, and the skip set in positional() decides whether its
@@ -3743,13 +3999,26 @@ function version() {
3743
3999
  return "unknown";
3744
4000
  }
3745
4001
  }
4002
+ function unknownCommand(typed) {
4003
+ const meant = didYouMean(typed, KNOWN_COMMANDS);
4004
+ return meant === void 0 ? `unknown command ${typed}` : `unknown command ${typed}; did you mean ${meant}?`;
4005
+ }
3746
4006
  function rejectUnknownFlags(argv) {
3747
- for (const token of argv) {
3748
- if (token === "--") {
3749
- return;
4007
+ const mine = ours(argv);
4008
+ for (let i = 0; i < mine.length; i += 1) {
4009
+ const token = mine[i] ?? "";
4010
+ if (TAKES_VALUE.has(token)) {
4011
+ i += 1;
4012
+ continue;
3750
4013
  }
3751
4014
  if (token.startsWith("-") && token !== "-" && !FLAGS.has(token)) {
3752
- throw new UsageError(`unknown flag ${token}`);
4015
+ if (PROXY_FLAGS.includes(token)) {
4016
+ throw new UsageError(`${token} is a flag for ${cliCommand()} proxy, not for this`);
4017
+ }
4018
+ const meant = didYouMean(token, [...FLAGS]);
4019
+ throw new UsageError(
4020
+ meant === void 0 ? `unknown flag ${token}` : `unknown flag ${token}; did you mean ${meant}?`
4021
+ );
3753
4022
  }
3754
4023
  }
3755
4024
  }
@@ -3779,6 +4048,13 @@ ${COMMANDS}`);
3779
4048
  return 0;
3780
4049
  }
3781
4050
  rejectUnknownFlags(argv);
4051
+ if (command !== void 0 && !KNOWN_COMMANDS.includes(command)) {
4052
+ throw new UsageError(unknownCommand(command));
4053
+ }
4054
+ const givenJournal = flag(argv, "--journal");
4055
+ const givenManifest = flag(argv, "--manifest");
4056
+ journalArg = givenJournal === void 0 ? "" : ` --journal ${resolve3(givenJournal)}`;
4057
+ manifestArg = givenManifest === void 0 ? "" : ` --manifest ${resolve3(givenManifest)}`;
3782
4058
  if (command === void 0) {
3783
4059
  const manifestPath = findManifest(flag(argv, "--manifest"));
3784
4060
  const journalPath2 = findJournal(flag(argv, "--journal"), manifestPath);
@@ -3847,7 +4123,6 @@ ${COMMANDS}`);
3847
4123
  decideAs: flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown"
3848
4124
  });
3849
4125
  }
3850
- journalArg = given === void 0 ? "" : ` --journal ${resolve3(given)}`;
3851
4126
  if (!existsSync8(journalPath) && (command === "list" || command === "show" || command === "gates")) {
3852
4127
  if (asJson) {
3853
4128
  out(JSON.stringify(command === "show" ? { run: null, actions: [] } : []));
@@ -3885,7 +4160,7 @@ ${COMMANDS}`);
3885
4160
  case "undo":
3886
4161
  return await runUndo(argv, journal);
3887
4162
  default:
3888
- throw new UsageError(`unknown command ${command}`);
4163
+ throw new UsageError(unknownCommand(command));
3889
4164
  }
3890
4165
  } finally {
3891
4166
  journal.close();
@@ -3912,8 +4187,7 @@ try {
3912
4187
  if (error instanceof UsageError) {
3913
4188
  process.stderr.write(
3914
4189
  error.listCommands ? `synartesis: ${error.message}
3915
-
3916
- ${COMMANDS}` : `synartesis: ${error.message}
4190
+ ${shortList()}` : `synartesis: ${error.message}
3917
4191
  `
3918
4192
  );
3919
4193
  process.exitCode = 2;
package/dist/proxy.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ PROXY_FLAGS,
3
4
  canonical,
4
5
  cliCommandFrom,
5
6
  connectStdioUpstream,
@@ -22,7 +23,7 @@ import {
22
23
  verifyAgainstServers,
23
24
  warnUntested,
24
25
  withIdempotencyKey
25
- } from "./chunk-6X7GPUGJ.js";
26
+ } from "./chunk-OODDXM36.js";
26
27
  import {
27
28
  SnapshotError,
28
29
  UpstreamError,
@@ -585,14 +586,21 @@ function createProxyServer(options) {
585
586
  enter();
586
587
  try {
587
588
  const wantsGate = policy.gate === "always" || policy.gate === "on_write" && shouldGateOnWrite(args);
588
- const granted = wantsGate ? journal.findApproval({
589
+ const unresolved = policy.class === "readonly" ? void 0 : journal.findPending({
590
+ runId: activeRun,
591
+ server: route.upstream.name,
592
+ tool: route.tool,
593
+ args
594
+ });
595
+ const mustAsk = wantsGate || unresolved !== void 0;
596
+ const granted = mustAsk ? journal.findApproval({
589
597
  server: route.upstream.name,
590
598
  tool: route.tool,
591
599
  args,
592
600
  notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
593
601
  }) : void 0;
594
602
  const inherited = granted !== void 0 && granted.runId !== activeRun ? granted : void 0;
595
- const waiting = granted === void 0 && wantsGate ? journal.findGated({
603
+ const waiting = granted === void 0 && mustAsk ? journal.findGated({
596
604
  runId: activeRun,
597
605
  server: route.upstream.name,
598
606
  tool: route.tool,
@@ -676,9 +684,13 @@ function createProxyServer(options) {
676
684
  );
677
685
  }
678
686
  };
679
- const askedAlready = wantsGate;
680
- if (wantsGate && (granted === void 0 || !spent)) {
681
- await decide("this action cannot be undone");
687
+ const askedAlready = mustAsk;
688
+ if (mustAsk && (granted === void 0 || !spent)) {
689
+ const because = [
690
+ unresolved === void 0 ? void 0 : `an earlier attempt at this exact call, action ${String(unresolved.seq)}, went out and its outcome could never be established, so sending it again may apply the change a second time`,
691
+ wantsGate ? "this action cannot be undone" : void 0
692
+ ].filter((one) => one !== void 0);
693
+ await decide(because.join(", and "));
682
694
  }
683
695
  let snapshot;
684
696
  let verify;
@@ -1062,7 +1074,7 @@ function parseArgv(argv) {
1062
1074
  const at = argv.indexOf(flag);
1063
1075
  return at === -1 ? void 0 : argv[at + 1];
1064
1076
  };
1065
- const known = ["--manifest", "--journal", "--server", "--gate-timeout", "--log-level", "--http", "--http-host", "--http-idle", "--token"];
1077
+ const known = PROXY_FLAGS;
1066
1078
  const unknown = argv.find((token) => token.startsWith("--") && !known.includes(token));
1067
1079
  if (unknown !== void 0) {
1068
1080
  throw new Error(`unknown flag ${unknown}; expected one of ${known.join(", ")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synartesis",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "description": "An undo layer for AI agents.",
5
5
  "type": "module",
6
6
  "private": false,