agent-yes 1.228.0 → 1.229.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 (45) hide show
  1. package/dist/{JsonlStore-Bm5Nj6v_.js → JsonlStore-CWOA3aDH.js} +1 -1
  2. package/dist/{SUPPORTED_CLIS-C5d64Xqj.js → SUPPORTED_CLIS-CvLtp6LS.js} +2 -2
  3. package/dist/SUPPORTED_CLIS-DJEnqsp2.js +11 -0
  4. package/dist/{agentShare-CpglhRYb.js → agentShare-CQwvWWfd.js} +4 -4
  5. package/dist/{callback-B3N2VJ1i.js → callback-BFZDARQq.js} +4 -4
  6. package/dist/{callback-CCEQtwXX.js → callback-BN-tH9dB.js} +3 -3
  7. package/dist/cli.js +9 -9
  8. package/dist/{cwdConflictWarn-CJZRp4S0.js → cwdConflictWarn-CKUsQ5_o.js} +2 -2
  9. package/dist/{globalPidIndex-CoNr7tS8.js → globalPidIndex-Ca4sTr1b.js} +1 -1
  10. package/dist/index.js +8 -8
  11. package/dist/{messageLog-Bb_Ie3Ck.js → messageLog-zVGWLX7w.js} +1 -1
  12. package/dist/{notifyDaemon-CfQN5blR.js → notifyDaemon-CENKHs1M.js} +4 -4
  13. package/dist/pidStore-Cp3HekUh.js +6 -0
  14. package/dist/{pidStore-DCqpU_bI.js → pidStore-Hjba3K6Q.js} +3 -3
  15. package/dist/{reaper-BNKvujYy.js → reaper-iQXyufgp.js} +1 -1
  16. package/dist/{reaper-BBc2AIKA.js → reaper-pJpMYKmj.js} +1 -1
  17. package/dist/{runningLock-CNMl13dC.js → runningLock-BFk3Mo8K.js} +1 -1
  18. package/dist/{rustBinary-B8IcqSw1.js → rustBinary-BjApGser.js} +2 -2
  19. package/dist/{schedule-B-RjgOOR.js → schedule-D0O3sb5A.js} +9 -9
  20. package/dist/{serve-DwF03sgB.js → serve-m-WChRrO.js} +67 -21
  21. package/dist/{setup-cZFxfS89.js → setup-fuwC5wlF.js} +2 -2
  22. package/dist/spawnGate-5jC6eiqk.js +5 -0
  23. package/dist/{spawnGate-CeFYH-Ib.js → spawnGate-CMlGqVN4.js} +2 -2
  24. package/dist/{subcommands-DSyCT0vl.js → subcommands-DzJbnYAx.js} +12 -12
  25. package/dist/{subcommands-UseeaeYM.js → subcommands-lwgJxJex.js} +3 -3
  26. package/dist/{todoAutomation-Cwnh4aWo.js → todoAutomation-x6guaQXc.js} +115 -6
  27. package/dist/{todoCli-6v1CYdxN.js → todoCli-Cjhx3DqQ.js} +38 -6
  28. package/dist/{tray-DBt8X-Qj.js → tray-C8VfnS7s.js} +2 -2
  29. package/dist/{ts-Cl3CmLH9.js → ts-oYrTk4Z_.js} +103 -20
  30. package/dist/{versionChecker-Dhjyjxs_.js → versionChecker-CgCXc8CJ.js} +2 -2
  31. package/dist/{ws-Dw2fGxTI.js → ws-D5q3R1kY.js} +4 -4
  32. package/package.json +1 -1
  33. package/ts/askApi.spec.ts +358 -0
  34. package/ts/askApi.ts +255 -0
  35. package/ts/index.ts +2 -0
  36. package/ts/serve.ts +131 -0
  37. package/ts/todoBlock.spec.ts +10 -0
  38. package/ts/todoBlock.ts +14 -2
  39. package/ts/todoCli.spec.ts +101 -0
  40. package/ts/todoCli.ts +69 -1
  41. package/ts/todoStore.spec.ts +126 -0
  42. package/ts/todoStore.ts +130 -2
  43. package/dist/SUPPORTED_CLIS-Bfresz1n.js +0 -11
  44. package/dist/pidStore-CatWvn5W.js +0 -6
  45. package/dist/spawnGate-CIC7b7nm.js +0 -5
package/ts/todoStore.ts CHANGED
@@ -87,6 +87,19 @@ export interface TodoRecord extends JsonlDoc {
87
87
  orphanedFrom?: string;
88
88
  /** Up to a few currently-idle agent ids suggested as replacements, computed at the moment of orphaning — a snapshot, not live; re-run reconcile for a fresh list. */
89
89
  reassignCandidates?: string[];
90
+ /**
91
+ * Incremented every time `block` is written (set OR cleared) — never
92
+ * reset, never derived from `block`'s own content. Exists specifically so
93
+ * a caller that read a block, did other work, and now wants to clear
94
+ * EXACTLY that block instance (`clearBlockIfMatches`) can detect an ABA
95
+ * race: another process replacing the block with a byte-for-byte
96
+ * IDENTICAL `{type, who, question/options/actionLink}` value would be
97
+ * invisible to a content-only comparison, silently erasing that (distinct,
98
+ * newer) block instance as if it were the one originally read
99
+ * (codex-review round-17 Important). Absent on records written before
100
+ * this field existed — treated as `0`.
101
+ */
102
+ blockRev?: number;
90
103
  createdAt: string;
91
104
  updatedAt: string;
92
105
  }
@@ -621,7 +634,11 @@ export class TodoStore {
621
634
  // reload-and-recompute-from-fresh dance the way `blockedBy` does below —
622
635
  // it still goes through the write lock via `rawUpdate` for existence
623
636
  // validation to run against a fresh record.
624
- return this.rawUpdate(id, () => ({ block }));
637
+ //
638
+ // `blockRev` bumps on every write regardless of whether `block` actually
639
+ // changed value — see the field's doc comment for why identity, not
640
+ // content, is what `clearBlockIfMatches` needs.
641
+ return this.rawUpdate(id, (fresh) => ({ block, blockRev: (fresh.blockRev ?? 0) + 1 }));
625
642
  }
626
643
 
627
644
  /**
@@ -641,7 +658,118 @@ export class TodoStore {
641
658
  `task ${id}: block changed since this was decided (expected waiting-on-agent for "${expectedAgentId}") — not clearing`,
642
659
  );
643
660
  }
644
- return { block: null };
661
+ return { block: null, blockRev: (fresh.blockRev ?? 0) + 1 };
662
+ });
663
+ }
664
+
665
+ /**
666
+ * Clears `block` ONLY if the FRESH record's `blockRev` still equals
667
+ * `expectedBlockRev` — the generalized form of `clearWaitingOnAgentBlock`
668
+ * above, for a caller (e.g. `askApi.ts`'s `answerAsk`) that decided to
669
+ * clear a specific `blocked-by-human` block instance from a snapshot
670
+ * taken at the start of a longer operation.
671
+ *
672
+ * Compares `blockRev`, NOT the block's own content: an earlier version of
673
+ * this method compared `JSON.stringify(fresh.block)` against the snapshot,
674
+ * which missed an ABA race — another process could replace the block with
675
+ * a byte-for-byte IDENTICAL `{type, who, question/options/actionLink}`
676
+ * value (e.g. the exact same question asked again) between the caller's
677
+ * read and this call, and the content-only check would see no difference,
678
+ * silently erasing that distinct, newer block instance as if it were the
679
+ * one the caller actually decided to answer (codex-review round-17
680
+ * Important). `blockRev` increments on every write to `block` regardless
681
+ * of whether the new value differs from the old one, so it uniquely
682
+ * identifies THIS PARTICULAR block-setting event, not just its shape.
683
+ *
684
+ * Clearing unconditionally by `id` alone (or by content alone) could
685
+ * silently erase a genuinely different, newer block that another process
686
+ * set in the meantime — the task would then look unblocked even though a
687
+ * real, later block exists (codex-review Important). Throws instead of
688
+ * silently no-op-ing so the caller can report the conflict rather than
689
+ * claim success.
690
+ */
691
+ clearBlockIfMatches(id: string, expectedBlockRev: number): Promise<TodoRecord> {
692
+ return this.rawUpdate(id, (fresh) => {
693
+ if ((fresh.blockRev ?? 0) !== expectedBlockRev) {
694
+ throw new Error(`task ${id}: block changed since this was decided — not clearing`);
695
+ }
696
+ return { block: null, blockRev: (fresh.blockRev ?? 0) + 1 };
697
+ });
698
+ }
699
+
700
+ /**
701
+ * Atomically clears a `blocked-by-human` block AND, if `gate` is given,
702
+ * appends its evidence and advances the transition it names — all inside
703
+ * ONE `rawUpdate` write (one lock acquisition, one reload, one write).
704
+ *
705
+ * This exists specifically so `askApi.ts`'s `answerAsk()` never composes
706
+ * "verify blockRev, approve() a gate, transition(), clearBlockIfMatches()"
707
+ * as separate locked writes again. That composition (this method's
708
+ * predecessor) had a real gap: `approve()`/`transition()` could durably
709
+ * succeed — evidence appended, state advanced — and ONLY THEN could the
710
+ * final `clearBlockIfMatches()` fail because another process replaced the
711
+ * block in between. The task was left in a state no caller intended:
712
+ * already transitioned/evidenced for the OLD block, while `block` still
713
+ * held a DIFFERENT, newer `blocked-by-human` ask that was never gated at
714
+ * all (codex-review round-18 Important — the exact race this module's
715
+ * whole `blockRev` mechanism exists to close, just one step further out
716
+ * than `clearBlockIfMatches` alone could reach). Folding everything into
717
+ * one `rawUpdate` cycle makes the two outcomes (nothing changed / gate +
718
+ * transition + clear all changed together) the only two possible ones —
719
+ * there is no window in which one succeeded and the other didn't.
720
+ *
721
+ * `blockRev` re-validates `canTransition` against the FRESH state (not a
722
+ * value captured before this call), matching `transition()`'s own
723
+ * concurrent-state-drift defense — a state change unrelated to `block`
724
+ * (and so invisible to the `blockRev` check alone) must still be caught.
725
+ *
726
+ * Independent verification (this module's single most important
727
+ * invariant — see the file's own header comment) is re-checked here
728
+ * against the FRESH owner, exactly as `approve()` checks it: `gate`, when
729
+ * given, is never applied if its `validator` is the task's own owner.
730
+ * This method does not call `approve()` (it is not a separate write), so
731
+ * skipping this check here would silently let a human answer their own
732
+ * gated ask with no independent-verification enforcement at all.
733
+ */
734
+ answerHumanBlock(
735
+ id: string,
736
+ expectedBlockRev: number,
737
+ gate: { name: string; toState: string; validator: string; note: string } | null,
738
+ ): Promise<TodoRecord> {
739
+ return this.rawUpdate(id, (fresh) => {
740
+ if ((fresh.blockRev ?? 0) !== expectedBlockRev) {
741
+ throw new Error(
742
+ `task ${id}: this ask has changed since it was loaded (expected blockRev ${expectedBlockRev}, current ${fresh.blockRev ?? 0}) — refresh and try again`,
743
+ );
744
+ }
745
+ const patch: Partial<Omit<TodoRecord, "_id">> = {
746
+ block: null,
747
+ blockRev: (fresh.blockRev ?? 0) + 1,
748
+ };
749
+ if (gate) {
750
+ if (
751
+ fresh.owner &&
752
+ fresh.owner.trim().toLowerCase() === gate.validator.trim().toLowerCase()
753
+ ) {
754
+ throw new Error(
755
+ `task ${id}: independent verification required — validator "${gate.validator}" is the same identity as owner "${fresh.owner}"; the worker cannot certify their own work`,
756
+ );
757
+ }
758
+ if (!canTransition(fresh.kind, fresh.state, gate.toState)) {
759
+ throw new Error(
760
+ `task ${id}: no transition ${fresh.state} -> ${gate.toState} for kind "${fresh.kind}" (state changed concurrently) — not clearing`,
761
+ );
762
+ }
763
+ const evidenceEntry: GateEvidence = {
764
+ gate: gate.name,
765
+ passedAt: new Date().toISOString(),
766
+ validator: gate.validator,
767
+ note: gate.note,
768
+ };
769
+ patch.verifyEvidence = [...fresh.verifyEvidence, evidenceEntry];
770
+ patch.state = gate.toState;
771
+ }
772
+ return patch;
645
773
  });
646
774
  }
647
775
 
@@ -1,11 +0,0 @@
1
- import "./ts-Cl3CmLH9.js";
2
- import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-Dhjyjxs_.js";
4
- import "./JsonlStore-Bm5Nj6v_.js";
5
- import "./globalPidIndex-CoNr7tS8.js";
6
- import "./pidStore-DCqpU_bI.js";
7
- import "./messageLog-Bb_Ie3Ck.js";
8
- import "./todoAutomation-Cwnh4aWo.js";
9
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C5d64Xqj.js";
10
-
11
- export { SUPPORTED_CLIS };
@@ -1,6 +0,0 @@
1
- import "./logger-CDIsZ-Pp.js";
2
- import "./JsonlStore-Bm5Nj6v_.js";
3
- import "./globalPidIndex-CoNr7tS8.js";
4
- import { t as PidStore } from "./pidStore-DCqpU_bI.js";
5
-
6
- export { PidStore };
@@ -1,5 +0,0 @@
1
- import "./logger-CDIsZ-Pp.js";
2
- import "./globalPidIndex-CoNr7tS8.js";
3
- import { i as waitForSpawnCapacity, n as spawnGateEnabled, r as spawnRejectionReason, t as memAvailableMb } from "./spawnGate-CeFYH-Ib.js";
4
-
5
- export { waitForSpawnCapacity };