@sema-agent/core 3.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +2382 -0
  2. package/dist/agents/cascade.d.ts +3 -1
  3. package/dist/agents/cascade.js +31 -32
  4. package/dist/agents/cumulative-stats.d.ts +29 -0
  5. package/dist/agents/cumulative-stats.js +29 -0
  6. package/dist/agents/observer.d.ts +4 -0
  7. package/dist/agents/observer.js +11 -10
  8. package/dist/agents/repair-loop.d.ts +2 -0
  9. package/dist/agents/repair-loop.js +48 -24
  10. package/dist/agents/retain-ledger.d.ts +2 -0
  11. package/dist/agents/retain-ledger.js +6 -5
  12. package/dist/agents/roster-store.d.ts +1 -0
  13. package/dist/agents/roster-store.js +3 -5
  14. package/dist/agents/send-message-tool.d.ts +1 -0
  15. package/dist/agents/send-message-tool.js +2 -0
  16. package/dist/agents/subagent.d.ts +3 -1
  17. package/dist/agents/subagent.js +79 -94
  18. package/dist/agents/teacher.d.ts +1 -9
  19. package/dist/agents/teacher.js +21 -24
  20. package/dist/agents/tool-filter.js +6 -7
  21. package/dist/agents/verify.d.ts +1 -0
  22. package/dist/agents/verify.js +15 -14
  23. package/dist/brain/anthropic.js +24 -21
  24. package/dist/brain/openai.js +24 -21
  25. package/dist/brain/stream-engine.js +5 -2
  26. package/dist/brain/stream-shared.d.ts +1 -0
  27. package/dist/brain/stream-shared.js +14 -0
  28. package/dist/brain/terminal-cause.d.ts +4 -0
  29. package/dist/brain/terminal-cause.js +12 -0
  30. package/dist/brain/tool-call-id.d.ts +1 -0
  31. package/dist/brain/tool-call-id.js +3 -0
  32. package/dist/brain/tool-call-repair.js +2 -1
  33. package/dist/core/arg-summary.d.ts +21 -1
  34. package/dist/core/arg-summary.js +61 -14
  35. package/dist/core/auto-compaction.d.ts +1 -0
  36. package/dist/core/auto-compaction.js +16 -25
  37. package/dist/core/background-agent-store.d.ts +22 -22
  38. package/dist/core/background-agent-store.js +19 -23
  39. package/dist/core/checkpoint-store.d.ts +6 -13
  40. package/dist/core/checkpoint-store.js +11 -5
  41. package/dist/core/file-snapshot-store.d.ts +8 -0
  42. package/dist/core/file-snapshot-store.js +4 -3
  43. package/dist/core/fs-write-gate-policy.js +2 -3
  44. package/dist/core/hooks.d.ts +4 -1
  45. package/dist/core/hooks.js +8 -18
  46. package/dist/core/lsp.js +4 -3
  47. package/dist/core/mailbox-store.d.ts +7 -10
  48. package/dist/core/mcp.d.ts +7 -10
  49. package/dist/core/mcp.js +7 -9
  50. package/dist/core/memory-engine/engine.d.ts +0 -1
  51. package/dist/core/memory-engine/engine.js +24 -34
  52. package/dist/core/memory-engine/file-backend.d.ts +1 -2
  53. package/dist/core/memory-engine/file-backend.js +25 -36
  54. package/dist/core/memory-engine/layout.d.ts +8 -0
  55. package/dist/core/memory-engine/layout.js +60 -4
  56. package/dist/core/memory-engine/scope-contract.js +9 -3
  57. package/dist/core/memory-engine/types.d.ts +1 -1
  58. package/dist/core/permission-rules.js +2 -3
  59. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  60. package/dist/core/runner/active-skill-scope.js +2 -17
  61. package/dist/core/runner/assemble-result.d.ts +2 -7
  62. package/dist/core/runner/assemble-result.js +3 -4
  63. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  64. package/dist/core/runner/compaction-call-options.js +30 -0
  65. package/dist/core/runner/prepare-task.d.ts +6 -23
  66. package/dist/core/runner/prepare-task.js +36 -61
  67. package/dist/core/runner/runtask.d.ts +1 -0
  68. package/dist/core/runner/runtask.js +84 -172
  69. package/dist/core/runner/session-rule-policy.js +7 -5
  70. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  71. package/dist/core/runner/teardown-bounded.js +36 -0
  72. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  73. package/dist/core/runner/turn-attachments.d.ts +5 -15
  74. package/dist/core/runner/turn-attachments.js +1 -1
  75. package/dist/core/safe-notify.d.ts +17 -0
  76. package/dist/core/safe-notify.js +59 -0
  77. package/dist/core/sensitive-path-policy.js +4 -4
  78. package/dist/core/session-reconcile.js +1 -2
  79. package/dist/core/skill-tool-specifier.js +2 -3
  80. package/dist/core/skills-directory.js +2 -3
  81. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  82. package/dist/core/stub-env.d.ts +2 -6
  83. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  84. package/dist/core/surrogate-safe-slice.js +18 -0
  85. package/dist/core/task-registry-monitor.js +8 -13
  86. package/dist/core/task-registry-shared.d.ts +23 -1
  87. package/dist/core/task-registry-shared.js +16 -0
  88. package/dist/core/task-registry.d.ts +6 -1
  89. package/dist/core/task-registry.js +52 -87
  90. package/dist/core/task-tool-shape.d.ts +0 -2
  91. package/dist/core/task-tool-shape.js +2 -5
  92. package/dist/core/tool-errors.js +5 -4
  93. package/dist/core/tool-name-aliases.d.ts +1 -2
  94. package/dist/core/tool-name-aliases.js +42 -59
  95. package/dist/core/tool-policy.js +21 -17
  96. package/dist/core/tool-result-store.d.ts +1 -0
  97. package/dist/core/tool-result-store.js +17 -1
  98. package/dist/core/trace.d.ts +9 -9
  99. package/dist/core/types.d.ts +15 -12
  100. package/dist/core/untrusted-egress.d.ts +4 -2
  101. package/dist/core/untrusted-egress.js +28 -14
  102. package/dist/core/workflow-journal-store.d.ts +9 -20
  103. package/dist/core/workflow-run-store.d.ts +11 -10
  104. package/dist/core/workflow-run-store.js +22 -16
  105. package/dist/engine/compaction/utils.js +4 -2
  106. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  107. package/dist/engine/execution-env/node-execution-env.js +1 -1
  108. package/dist/engine/harness/agent-harness.d.ts +1 -0
  109. package/dist/engine/harness/agent-harness.js +13 -33
  110. package/dist/engine/harness/types.d.ts +29 -67
  111. package/dist/engine/llm/types.d.ts +1 -1
  112. package/dist/engine/loop/agent-loop.js +12 -14
  113. package/dist/engine/loop/types.d.ts +11 -9
  114. package/dist/engine/session/memory-repo.d.ts +2 -6
  115. package/dist/engine/session/session.d.ts +5 -30
  116. package/dist/index.d.ts +6 -3
  117. package/dist/index.js +4 -3
  118. package/dist/internal/harness-types.d.ts +3 -1
  119. package/dist/orchestration/run-spec.js +6 -4
  120. package/dist/orchestration/run-workflow-tool.js +3 -36
  121. package/dist/orchestration/workflow-governance.js +2 -1
  122. package/dist/orchestration/workflow.d.ts +4 -11
  123. package/dist/orchestration/workflow.js +164 -204
  124. package/dist/prompt-assembly/packs/sema-default.js +1 -3
  125. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  126. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  127. package/dist/prompts/default.js +1 -3
  128. package/dist/prompts/simple-sections.d.ts +0 -1
  129. package/dist/prompts/simple-sections.js +0 -1
  130. package/dist/stores/file/background-agent-store.d.ts +3 -12
  131. package/dist/stores/file/background-agent-store.js +3 -41
  132. package/dist/stores/file/checkpoint-store.js +6 -6
  133. package/dist/stores/file/mailbox-store.d.ts +2 -6
  134. package/dist/stores/file/session-policy-store.js +15 -1
  135. package/dist/stores/file/session-store.d.ts +2 -6
  136. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  137. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  138. package/dist/stores/file/workflow-run-store.js +4 -17
  139. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  140. package/dist/tools/fs/bash-readonly-classifier.js +60 -25
  141. package/dist/tools/fs/fs-bash.js +330 -104
  142. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  143. package/dist/tools/fs/fs-shared.js +2 -2
  144. package/dist/tools/fs/fs-write.js +0 -1
  145. package/dist/tools/fs/notebook.d.ts +2 -5
  146. package/dist/tools/fs/safety.d.ts +3 -0
  147. package/dist/tools/fs/safety.js +43 -14
  148. package/dist/tools/fs/search.js +5 -5
  149. package/dist/tools/monitor.d.ts +1 -0
  150. package/dist/tools/monitor.js +7 -5
  151. package/dist/tools/worktree.d.ts +2 -7
  152. package/dist/tools/worktree.js +3 -1
  153. package/package.json +6 -2
@@ -1,7 +1,6 @@
1
1
  export declare const SIMPLE_COMMUNICATING_FABLE = "# Communicating with the user\n\nYour text output is what the user reads; they usually can't see your thinking or the raw tool results. Write it for a teammate who stepped away and is catching up, not for a log file: they don't know the codenames or shorthand you created along the way, and they didn't watch your process unfold. Before your first tool call, say in a sentence what you're about to do; while working, give brief updates when you find something load-bearing or change direction.\n\nText you write between tool calls may not be shown to the user. Everything the user needs from this turn \u2014 answers, summaries, findings, conclusions, deliverables \u2014 must be in the final text message of your turn, with no tool calls after it. Keep text between tool calls to brief status notes. If something important appeared only mid-turn or in your thinking, restate it in that final message.\n\nLead with the outcome. Your first sentence after finishing should answer \"what happened\" or \"what did you find\" \u2014 the thing the user would ask for if they said \"just give me the TLDR.\" Supporting detail and reasoning come after, for readers who want them.\n\nBeing readable and being concise are different things, and readable matters more. If the user has to reread your summary or ask you to explain, any time saved by brevity is gone. The way to keep output short is to be selective about what you include (drop details that don't change what the reader would do next), not to compress the writing into fragments, abbreviations, arrow chains like `A \u2192 B \u2192 fails`, or jargon. What you do include, write in complete sentences with the technical terms spelled out. Don't make the reader cross-reference labels or numbering you invented earlier; say what you mean in place.\n\nMatch the response to the question: a simple question gets a direct answer in prose, not headers and sections. Use tables only for short enumerable facts, with explanations in the surrounding prose rather than the cells. Calibrate to the user \u2014 a bit tighter for an expert, more explanatory for someone newer.\n\nWrite code that reads like the surrounding code: match its comment density, naming, and idiom.\nOnly write a code comment to state a constraint the code itself can't show \u2014 never to say where it came from, what the next line does, or why your change is correct; that's you talking to the reviewer, not the next reader, and it's noise the moment the PR merges.";
2
2
  export declare const SIMPLE_COMMUNICATING_LEAN = "Write code that reads like the surrounding code: match its comment density, naming, and idiom.";
3
3
  export declare const SIMPLE_ACTION_CAUTION = "For actions that are hard to reverse or outward-facing, confirm first unless durably authorized or explicitly told to proceed without asking; approval in one context doesn't extend to the next. Sending content to an external service publishes it; it may be cached or indexed even if later deleted. Before deleting or overwriting, look at the target \u2014 if what you find contradicts how it was described, or you didn't create it, surface that instead of proceeding. Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.";
4
- export declare const SIMPLE_TASK_CONTINUITY = "When a task has been agreed, the approval covers it end to end \u2014 in-scope steps don't need re-confirmation (irreversible or shared-system actions still do). Announcing a step without the tool call in the same turn hands control back with the work still pending; if the next step is decided, run it. Hand back only when done, waiting on something external, or the next step needs the user's decision. If the user asks something mid-task, answer and continue.";
5
4
  export declare const SIMPLE_INVESTIGATE_FIRST = "Asking the user a clarifying question has a cost: it interrupts them, and often they could have answered it themselves with a grep. Before asking, spend up to a minute on read-only investigation (grep the codebase, check docs, search memory) so your question is specific. \"I found tunnels X and Y in the config \u2014 which one?\" beats \"what tunnel?\"";
6
5
  export declare const SIMPLE_CONTEXT_MANAGEMENT = "# Context management\nWhen the conversation grows long, some or all of the current context is summarized; the summary, along with any remaining unsummarized context, is provided in the next context window so work can continue \u2014 you don't need to wrap up early or hand off mid-task.";
7
6
  export declare const SIMPLE_ACT_DONT_REDERIVE = "When you have enough information to act, act. Do not re-derive facts already established in the conversation, re-litigate a decision the user has already made, or narrate options you will not pursue. If you are weighing a choice, give a recommendation, not an exhaustive survey";
@@ -14,7 +14,6 @@ Write code that reads like the surrounding code: match its comment density, nami
14
14
  Only write a code comment to state a constraint the code itself can't show — never to say where it came from, what the next line does, or why your change is correct; that's you talking to the reviewer, not the next reader, and it's noise the moment the PR merges.`;
15
15
  export const SIMPLE_COMMUNICATING_LEAN = "Write code that reads like the surrounding code: match its comment density, naming, and idiom.";
16
16
  export const SIMPLE_ACTION_CAUTION = "For actions that are hard to reverse or outward-facing, confirm first unless durably authorized or explicitly told to proceed without asking; approval in one context doesn't extend to the next. Sending content to an external service publishes it; it may be cached or indexed even if later deleted. Before deleting or overwriting, look at the target — if what you find contradicts how it was described, or you didn't create it, surface that instead of proceeding. Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.";
17
- export const SIMPLE_TASK_CONTINUITY = "When a task has been agreed, the approval covers it end to end — in-scope steps don't need re-confirmation (irreversible or shared-system actions still do). Announcing a step without the tool call in the same turn hands control back with the work still pending; if the next step is decided, run it. Hand back only when done, waiting on something external, or the next step needs the user's decision. If the user asks something mid-task, answer and continue.";
18
17
  export const SIMPLE_INVESTIGATE_FIRST = 'Asking the user a clarifying question has a cost: it interrupts them, and often they could have answered it themselves with a grep. Before asking, spend up to a minute on read-only investigation (grep the codebase, check docs, search memory) so your question is specific. "I found tunnels X and Y in the config — which one?" beats "what tunnel?"';
19
18
  export const SIMPLE_CONTEXT_MANAGEMENT = `# Context management
20
19
  When the conversation grows long, some or all of the current context is summarized; the summary, along with any remaining unsummarized context, is provided in the next context window so work can continue — you don't need to wrap up early or hand off mid-task.`;
@@ -1,4 +1,4 @@
1
- import { type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentStore } from "../../core/background-agent-store.js";
1
+ import { type BackgroundAgentReapOptions, type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentStore, type BackgroundAgentUpdateIfExpect } from "../../core/background-agent-store.js";
2
2
  export interface FileBackgroundAgentStoreOptions {
3
3
  fsync?: boolean;
4
4
  compactEvery?: number;
@@ -18,12 +18,7 @@ export declare class FileBackgroundAgentStore implements BackgroundAgentStore {
18
18
  update(handle: string, scope: string, record: BackgroundAgentRecord, expect: {
19
19
  rev: number;
20
20
  }): Promise<boolean>;
21
- updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: {
22
- rev: number;
23
- status?: BackgroundAgentRecord["status"];
24
- parkedCheckpointToken?: string | null;
25
- parkClaimId?: string | null;
26
- }): Promise<boolean>;
21
+ updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: BackgroundAgentUpdateIfExpect): Promise<boolean>;
27
22
  listBySession(scope: string, sessionId: string, opts?: {
28
23
  status?: BackgroundAgentRecord["status"];
29
24
  limit?: number;
@@ -36,11 +31,7 @@ export declare class FileBackgroundAgentStore implements BackgroundAgentStore {
36
31
  delete(handle: string, scope: string, expect?: {
37
32
  rev: number;
38
33
  }): Promise<boolean>;
39
- reap(scope: string, now: number, opts?: {
40
- maxAgeMs?: number;
41
- keep?: number;
42
- staleRunningMaxAgeMs?: number;
43
- }): Promise<number>;
34
+ reap(scope: string, now: number, opts?: BackgroundAgentReapOptions): Promise<number>;
44
35
  compactNow(): void;
45
36
  get size(): number;
46
37
  close(): void;
@@ -1,5 +1,5 @@
1
1
  import { join } from "node:path";
2
- import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, } from "../../core/background-agent-store.js";
2
+ import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, queryBackgroundAgents, } from "../../core/background-agent-store.js";
3
3
  import { SharedLedgerTable } from "./shared-ledger.js";
4
4
  const agentLedgers = new SharedLedgerTable({
5
5
  keyOf: (r) => FileBackgroundAgentStore.key(r.handle, r.scope),
@@ -92,31 +92,10 @@ export class FileBackgroundAgentStore {
92
92
  });
93
93
  }
94
94
  async listBySession(scope, sessionId, opts) {
95
- const out = [];
96
- for (const r of this.rows.values()) {
97
- if (r.scope !== scope)
98
- continue;
99
- const anchor = r.sessionScoped ? r.owner : r.parentSessionId;
100
- if (anchor !== sessionId && r.rootSessionId !== sessionId)
101
- continue;
102
- if (opts?.status !== undefined && r.status !== opts.status)
103
- continue;
104
- out.push(toSummary(r));
105
- }
106
- out.sort((a, b) => b.spawnedAt - a.spawnedAt);
107
- return opts?.limit !== undefined ? out.slice(0, Math.max(0, opts.limit)) : out;
95
+ return queryBackgroundAgents(this.rows.values(), { scope, sessionId, status: opts?.status, limit: opts?.limit });
108
96
  }
109
97
  async listByScope(scope, opts) {
110
- const out = [];
111
- for (const r of this.rows.values()) {
112
- if (r.scope !== scope)
113
- continue;
114
- if (opts?.status !== undefined && r.status !== opts.status)
115
- continue;
116
- out.push(toSummary(r));
117
- }
118
- out.sort((a, b) => b.spawnedAt - a.spawnedAt);
119
- return opts?.limit !== undefined ? out.slice(0, Math.max(0, opts.limit)) : out;
98
+ return queryBackgroundAgents(this.rows.values(), { scope, status: opts?.status, limit: opts?.limit });
120
99
  }
121
100
  async listScopes() {
122
101
  return [...new Set([...this.rows.values()].map((r) => r.scope))].sort();
@@ -205,20 +184,3 @@ export class FileBackgroundAgentStore {
205
184
  this.ledger.release();
206
185
  }
207
186
  }
208
- function toSummary(r) {
209
- return {
210
- handle: r.handle,
211
- owner: r.owner,
212
- sessionScoped: r.sessionScoped,
213
- ...(r.sessionId !== undefined ? { sessionId: r.sessionId } : {}),
214
- ...(r.parentSessionId !== undefined ? { parentSessionId: r.parentSessionId } : {}),
215
- ...(r.rootSessionId !== undefined ? { rootSessionId: r.rootSessionId } : {}),
216
- ...(r.name !== undefined ? { name: r.name } : {}),
217
- ...(r.agentType !== undefined ? { agentType: r.agentType } : {}),
218
- status: r.status,
219
- spawnedAt: r.spawnedAt,
220
- updatedAt: r.updatedAt,
221
- ...(r.settledAt !== undefined ? { settledAt: r.settledAt } : {}),
222
- ...(r.usage !== undefined ? { usage: structuredClone(r.usage) } : {}),
223
- };
224
- }
@@ -1,5 +1,5 @@
1
1
  import { join } from "node:path";
2
- import { CheckpointError, summarizeCheckpoint, validatePendingSteer, winnerFromOutcome, } from "../../core/checkpoint-store.js";
2
+ import { CheckpointError, checkpointOccMatches, checkpointRowMatches, summarizeCheckpoint, validatePendingSteer, winnerFromOutcome, } from "../../core/checkpoint-store.js";
3
3
  import { SharedLedgerTable } from "./shared-ledger.js";
4
4
  function applyCheckpointEvent(cps, ev) {
5
5
  if (ev.t === "put") {
@@ -80,10 +80,10 @@ export class FileCheckpointStore {
80
80
  throw new Error("injected fault: resolve before commit");
81
81
  }
82
82
  const cp = this.cps.get(token);
83
- if (!cp || cp.scope !== scope || cp.status !== "pending") {
83
+ if (!checkpointRowMatches(cp, scope, "pending")) {
84
84
  return false;
85
85
  }
86
- if (expect !== undefined && (cp.rev ?? 0) !== expect.rev) {
86
+ if (!checkpointOccMatches(cp, expect)) {
87
87
  return false;
88
88
  }
89
89
  const winner = winnerFromOutcome(outcome);
@@ -99,7 +99,7 @@ export class FileCheckpointStore {
99
99
  async reopen(token, scope, reason) {
100
100
  return this.withLock(token, () => {
101
101
  const cp = this.cps.get(token);
102
- if (!cp || cp.scope !== scope || cp.status !== "resolved") {
102
+ if (!checkpointRowMatches(cp, scope, "resolved")) {
103
103
  return false;
104
104
  }
105
105
  const rev = (cp.rev ?? 0) + 1;
@@ -111,7 +111,7 @@ export class FileCheckpointStore {
111
111
  const clean = validatePendingSteer(steer);
112
112
  return this.withLock(token, () => {
113
113
  const cp = this.cps.get(token);
114
- if (!cp || cp.scope !== scope || cp.status !== "pending") {
114
+ if (!checkpointRowMatches(cp, scope, "pending")) {
115
115
  return false;
116
116
  }
117
117
  this.commit({ t: "steer", token, steer: clean }, this.fsyncEnabled);
@@ -121,7 +121,7 @@ export class FileCheckpointStore {
121
121
  async expire(token, scope) {
122
122
  return this.withLock(token, () => {
123
123
  const cp = this.cps.get(token);
124
- if (!cp || cp.scope !== scope || cp.status !== "pending") {
124
+ if (!checkpointRowMatches(cp, scope, "pending")) {
125
125
  return false;
126
126
  }
127
127
  this.commit({ t: "expire", token }, true);
@@ -1,4 +1,4 @@
1
- import { type MailboxLease, type MailboxStore } from "../../core/mailbox-store.js";
1
+ import { type MailboxAppendMessage, type MailboxLease, type MailboxStore } from "../../core/mailbox-store.js";
2
2
  export interface FileMailboxStoreOptions {
3
3
  fsync?: boolean;
4
4
  compactEvery?: number;
@@ -15,11 +15,7 @@ export declare class FileMailboxStore implements MailboxStore {
15
15
  private load;
16
16
  private commit;
17
17
  private compact;
18
- append(scope: string, handle: string, msg: {
19
- from?: string;
20
- content: string;
21
- sentAt: number;
22
- }): Promise<number>;
18
+ append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
23
19
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
24
20
  ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
25
21
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
@@ -105,11 +105,25 @@ export class FileSessionPolicyStore {
105
105
  for (const file of files) {
106
106
  if (!file.endsWith(".json"))
107
107
  continue;
108
+ let raw;
109
+ try {
110
+ raw = readFileSync(join(this.dir, file), "utf8");
111
+ }
112
+ catch {
113
+ continue;
114
+ }
108
115
  let parsed;
109
116
  try {
110
- parsed = JSON.parse(readFileSync(join(this.dir, file), "utf8"));
117
+ parsed = JSON.parse(raw);
111
118
  }
112
119
  catch {
120
+ try {
121
+ unlinkSync(join(this.dir, file));
122
+ }
123
+ catch (err) {
124
+ if (err.code !== "ENOENT")
125
+ throw err;
126
+ }
113
127
  continue;
114
128
  }
115
129
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
@@ -1,4 +1,4 @@
1
- import type { Session, SessionMetadata, SessionRepo, SessionTreeEntry } from "../../internal/harness.js";
1
+ import type { Session, SessionForkOptions, SessionMetadata, SessionRepo, SessionTreeEntry } from "../../internal/harness.js";
2
2
  export declare class FileSessionRepo implements SessionRepo {
3
3
  private readonly dir;
4
4
  private readonly tmpDir;
@@ -14,11 +14,7 @@ export declare class FileSessionRepo implements SessionRepo {
14
14
  open(metadata: SessionMetadata): Promise<Session>;
15
15
  list(): Promise<SessionMetadata[]>;
16
16
  delete(metadata: SessionMetadata): Promise<void>;
17
- fork(sourceMetadata: SessionMetadata, options?: {
18
- entryId?: string;
19
- position?: "before" | "at";
20
- id?: string;
21
- }): Promise<Session>;
17
+ fork(sourceMetadata: SessionMetadata, options?: SessionForkOptions): Promise<Session>;
22
18
  exportEntries(sessionId: string): Promise<SessionTreeEntry[]>;
23
19
  importEntries(sessionId: string, owner: string | undefined, entries: SessionTreeEntry[]): Promise<void>;
24
20
  }
@@ -1,4 +1,4 @@
1
- import { type WorkflowJournalEntry, type WorkflowJournalStore } from "../../core/workflow-journal-store.js";
1
+ import { type WorkflowJournalEntry, type WorkflowJournalStore, type ResumeClaimArgs } from "../../core/workflow-journal-store.js";
2
2
  export { MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "../../core/workflow-journal-store.js";
3
3
  export declare const RESUME_CLAIM_TTL_MS: number;
4
4
  export declare const RESUME_CLAIM_RELEASE_GRACE_MS = 5000;
@@ -24,19 +24,11 @@ export declare class FileWorkflowJournalStore implements WorkflowJournalStore {
24
24
  private readClaimRaw;
25
25
  private unlinkIfUnchanged;
26
26
  private sweepExpiredClaims;
27
- resumeClaim(input: {
28
- sourceRunId: string;
29
- newRunId: string;
30
- scope: string;
31
- }): Promise<{
27
+ resumeClaim(input: ResumeClaimArgs): Promise<{
32
28
  granted: boolean;
33
29
  holder?: string;
34
30
  }>;
35
- releaseResumeClaim(input: {
36
- sourceRunId: string;
37
- newRunId: string;
38
- scope: string;
39
- }): Promise<void>;
31
+ releaseResumeClaim(input: ResumeClaimArgs): Promise<void>;
40
32
  deleteByRun(runId: string): Promise<number>;
41
33
  dispose(): void;
42
34
  }
@@ -1,5 +1,5 @@
1
- import { type WorkflowRunStore, type WorkflowRunSummary } from "../../core/workflow-run-store.js";
2
- import type { WorkflowRun, WorkflowRunStatus } from "../../orchestration/workflow.js";
1
+ import { type WorkflowRunListByScopeOptions, type WorkflowRunStore, type WorkflowRunSummary } from "../../core/workflow-run-store.js";
2
+ import type { WorkflowRun } from "../../orchestration/workflow.js";
3
3
  export interface FileWorkflowRunStoreOptions {
4
4
  fsync?: boolean;
5
5
  compactEvery?: number;
@@ -18,11 +18,7 @@ export declare class FileWorkflowRunStore implements WorkflowRunStore {
18
18
  update(id: string, scope: string, run: WorkflowRun, expect?: {
19
19
  rev: number;
20
20
  }): Promise<boolean>;
21
- listByScope(scope: string, opts?: {
22
- status?: WorkflowRunStatus;
23
- limit?: number;
24
- session?: string;
25
- }): Promise<WorkflowRunSummary[]>;
21
+ listByScope(scope: string, opts?: WorkflowRunListByScopeOptions): Promise<WorkflowRunSummary[]>;
26
22
  reap(scope: string, now: number, opts?: {
27
23
  maxAgeMs?: number;
28
24
  keep?: number;
@@ -1,5 +1,5 @@
1
1
  import { join } from "node:path";
2
- import { WorkflowRunStoreError, isTerminalWorkflowStatus, summarizeWorkflowRun, } from "../../core/workflow-run-store.js";
2
+ import { WorkflowRunStoreError, isTerminalWorkflowStatus, nextWorkflowRunOnUpdate, queryWorkflowRuns, } from "../../core/workflow-run-store.js";
3
3
  import { SharedLedgerTable } from "./shared-ledger.js";
4
4
  const runLedgers = new SharedLedgerTable({
5
5
  keyOf: (run) => run.id,
@@ -53,28 +53,15 @@ export class FileWorkflowRunStore {
53
53
  }
54
54
  async update(id, scope, run, expect) {
55
55
  return this.withLock(id, () => {
56
- const cur = this.runs.get(id);
57
- if (!cur || cur.scope !== scope) {
56
+ const next = nextWorkflowRunOnUpdate(this.runs.get(id), id, scope, run, expect);
57
+ if (!next)
58
58
  return false;
59
- }
60
- if (expect !== undefined && (cur.rev ?? 0) !== expect.rev) {
61
- return false;
62
- }
63
- const next = structuredClone(run);
64
- next.id = id;
65
- next.scope = scope;
66
- next.rev = (cur.rev ?? 0) + 1;
67
59
  this.commit({ t: "update", id, run: next }, this.fsyncEnabled);
68
60
  return true;
69
61
  });
70
62
  }
71
63
  async listByScope(scope, opts) {
72
- const rows = [...this.runs.values()].filter((r) => r.scope === scope &&
73
- (opts?.status === undefined || r.status === opts.status) &&
74
- (opts?.session === undefined || r.originatingSessionId === opts.session));
75
- rows.sort((a, b) => b.createdAt - a.createdAt);
76
- const limited = opts?.limit !== undefined ? rows.slice(0, Math.max(0, opts.limit)) : rows;
77
- return limited.map(summarizeWorkflowRun);
64
+ return queryWorkflowRuns(this.runs.values(), scope, opts);
78
65
  }
79
66
  async reap(scope, now, opts) {
80
67
  if (opts?.maxAgeMs === undefined && opts?.keep === undefined)
@@ -15,6 +15,7 @@ export interface CompoundReadonlyVerdict {
15
15
  outOfRootRead?: true;
16
16
  outOfRootPaths?: readonly string[];
17
17
  checkedPaths?: readonly string[];
18
+ undecidedPaths?: readonly string[];
18
19
  }
19
20
  export declare function formatOutOfRootReadApprovalOption(directory: string): string;
20
21
  export declare function classifyCompoundReadonlyDetailed(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
@@ -1,4 +1,4 @@
1
- import { isBlockedDevicePath, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
1
+ import { isAbsolutePathForm, isBlockedDevicePath, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
2
2
  export const BASH_READONLY_DEFAULT_ALLOW = [
3
3
  "ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
4
4
  "grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
@@ -56,31 +56,24 @@ export function formatOutOfRootReadApprovalOption(directory) {
56
56
  return `Yes, allow reading from ${leaf || directory}/ from this project`;
57
57
  }
58
58
  const NO_PATH_OPERAND_COMMANDS = new Set(["pwd", "echo", "whoami", "uname", "which", "tr", "basename", "dirname"]);
59
- function isAbsolutePathToken(p) {
60
- return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
61
- }
62
59
  function isPathShapedToken(tok) {
63
60
  return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
64
61
  }
65
- function normalizeAbsPathLexicalEitherFamily(p) {
66
- const s = p.replace(/\\/g, "/");
67
- return /^[A-Za-z]:\//.test(s) ? s.slice(0, 2) + normalizeAbsPathLexically(s.slice(2)) : normalizeAbsPathLexically(s);
68
- }
69
62
  function resolveOperandLexically(base, operand, homeDir) {
70
63
  let raw = operand;
71
64
  if (raw === "~" || raw.startsWith("~/")) {
72
- if (homeDir === undefined || !isAbsolutePathToken(homeDir))
65
+ if (homeDir === undefined || !isAbsolutePathForm(homeDir))
73
66
  return undefined;
74
67
  raw = raw === "~" ? homeDir : `${homeDir.replace(/[/\\]+$/, "")}/${raw.slice(2)}`;
75
68
  }
76
69
  else if (raw.startsWith("~")) {
77
70
  return undefined;
78
71
  }
79
- if (isAbsolutePathToken(raw))
80
- return normalizeAbsPathLexicalEitherFamily(raw);
81
- if (base === undefined || !isAbsolutePathToken(base))
72
+ if (isAbsolutePathForm(raw))
73
+ return normalizeAbsPathLexically(raw);
74
+ if (base === undefined || !isAbsolutePathForm(base))
82
75
  return undefined;
83
- return normalizeAbsPathLexicalEitherFamily(`${base.replace(/[/\\]+$/, "")}/${raw}`);
76
+ return normalizeAbsPathLexically(`${base.replace(/[/\\]+$/, "")}/${raw}`);
84
77
  }
85
78
  function tokenizeSegment(segment) {
86
79
  const raw = segment
@@ -107,6 +100,22 @@ function hasUnquotedExpansionMetachar(rawToken) {
107
100
  }
108
101
  return false;
109
102
  }
103
+ function hasUnquotedGlobMetachar(rawToken) {
104
+ let open;
105
+ for (const ch of rawToken) {
106
+ if (open === undefined && (ch === '"' || ch === "'")) {
107
+ open = ch;
108
+ continue;
109
+ }
110
+ if (open === ch) {
111
+ open = undefined;
112
+ continue;
113
+ }
114
+ if (open === undefined && (ch === "*" || ch === "?" || ch === "["))
115
+ return true;
116
+ }
117
+ return false;
118
+ }
110
119
  function longOptionNameOf(tok) {
111
120
  if (!tok.startsWith("--") || tok.length === 2)
112
121
  return undefined;
@@ -180,6 +189,10 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
180
189
  const findings = [];
181
190
  const candidates = [];
182
191
  const bareWordOperands = [];
192
+ const argGlobs = (k) => {
193
+ const raw = tokens.raw[k + 1];
194
+ return raw !== undefined && hasUnquotedGlobMetachar(raw);
195
+ };
183
196
  for (const rawArg of tokens.raw.slice(1)) {
184
197
  if (!hasUnquotedExpansionMetachar(rawArg))
185
198
  continue;
@@ -191,14 +204,18 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
191
204
  ];
192
205
  }
193
206
  if (name === "cd") {
194
- const target = args.find((t) => !t.startsWith("-") || t === "-");
207
+ const targetIdx = args.findIndex((t) => !t.startsWith("-") || t === "-");
208
+ const target = targetIdx === -1 ? undefined : args[targetIdx];
209
+ if (target !== undefined && argGlobs(targetIdx)) {
210
+ return [{ kind: "unresolvable", reason: `\`cd\` is given the pattern "${target}", which the shell expands to a directory this check cannot know — not auto-allowed` }];
211
+ }
195
212
  if (target === undefined) {
196
213
  return [{ kind: "unresolvable", reason: '`cd` with no argument targets the home directory, which cannot be checked against the allowed directories — not auto-allowed' }];
197
214
  }
198
215
  if (target === "-") {
199
216
  return [{ kind: "unresolvable", reason: '`cd -` targets the previous working directory, which cannot be resolved statically — not auto-allowed' }];
200
217
  }
201
- candidates.push(target);
218
+ candidates.push({ text: target, globbed: false });
202
219
  }
203
220
  else {
204
221
  const patternSuppliedByFlag = name === "grep" && args.some(isGrepPatternFlagToken);
@@ -215,8 +232,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
215
232
  continue;
216
233
  if (t.startsWith("-") && t !== "-") {
217
234
  for (const payload of attachedOptionPayloads(t)) {
218
- if (isAbsolutePathToken(payload) || isPathShapedToken(payload))
219
- candidates.push(payload);
235
+ if (isAbsolutePathForm(payload) || isPathShapedToken(payload))
236
+ candidates.push({ text: payload, globbed: argGlobs(k) });
220
237
  }
221
238
  continue;
222
239
  }
@@ -227,12 +244,18 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
227
244
  if (isGrepPatternSlot)
228
245
  continue;
229
246
  if (isPathShapedToken(t))
230
- candidates.push(t);
247
+ candidates.push({ text: t, globbed: argGlobs(k) });
231
248
  else
232
- bareWordOperands.push(t);
249
+ bareWordOperands.push({ text: t, globbed: argGlobs(k) });
233
250
  }
234
251
  }
235
- for (const candidate of candidates) {
252
+ for (let k = 0; k < args.length; k++) {
253
+ if (!argGlobs(k))
254
+ continue;
255
+ const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], args[k], boundary.homeDir);
256
+ findings.push({ kind: "undecided", path: resolved ?? args[k] });
257
+ }
258
+ for (const { text: candidate, globbed } of candidates) {
236
259
  const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], candidate, boundary.homeDir);
237
260
  if (resolved === undefined) {
238
261
  findings.push({
@@ -241,14 +264,18 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
241
264
  });
242
265
  continue;
243
266
  }
244
- if (withinAnyRoot(boundary.roots, resolved))
245
- findings.push({ kind: "inside", path: resolved });
267
+ if (withinAnyRoot(boundary.roots, resolved)) {
268
+ if (!globbed)
269
+ findings.push({ kind: "inside", path: resolved });
270
+ }
246
271
  else
247
272
  findings.push({ kind: "outside", command: name, path: resolved });
248
273
  }
249
- for (const operand of bareWordOperands) {
274
+ for (const { text: operand, globbed } of bareWordOperands) {
250
275
  const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
251
- if (resolved !== undefined && withinAnyRoot(boundary.roots, resolved))
276
+ if (resolved === undefined)
277
+ continue;
278
+ if (!globbed && withinAnyRoot(boundary.roots, resolved))
252
279
  findings.push({ kind: "inside", path: resolved });
253
280
  }
254
281
  return findings;
@@ -394,6 +421,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
394
421
  function evaluateReadBoundary(foldedSegments, boundary) {
395
422
  const outside = [];
396
423
  const inside = [];
424
+ const undecided = [];
397
425
  for (const toks of foldedSegments) {
398
426
  for (const finding of collectSegmentBoundaryFindings(toks, boundary)) {
399
427
  if (finding.kind === "unresolvable")
@@ -403,12 +431,18 @@ function evaluateReadBoundary(foldedSegments, boundary) {
403
431
  inside.push(finding.path);
404
432
  continue;
405
433
  }
434
+ if (finding.kind === "undecided") {
435
+ if (!undecided.includes(finding.path))
436
+ undecided.push(finding.path);
437
+ continue;
438
+ }
406
439
  if (!outside.some((o) => o.path === finding.path))
407
440
  outside.push(finding);
408
441
  }
409
442
  }
443
+ const undecidedField = undecided.length > 0 ? { undecidedPaths: undecided } : {};
410
444
  if (outside.length === 0)
411
- return inside.length > 0 ? { checkedPaths: inside } : {};
445
+ return inside.length > 0 ? { checkedPaths: inside, ...undecidedField } : { ...undecidedField };
412
446
  const paths = outside.map((o) => `"${o.path}"`).join(", ");
413
447
  const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
414
448
  return {
@@ -416,6 +450,7 @@ function evaluateReadBoundary(foldedSegments, boundary) {
416
450
  outOfRootRead: true,
417
451
  outOfRootPaths: outside.map((o) => o.path),
418
452
  ...(inside.length > 0 ? { checkedPaths: inside } : {}),
453
+ ...undecidedField,
419
454
  };
420
455
  }
421
456
  export function classifySimpleCommandReadBoundary(command, boundary) {