@namzu/sdk 44.0.0 → 44.2.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 (72) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  3. package/dist/agents/ReactiveAgent.js +4 -0
  4. package/dist/agents/ReactiveAgent.js.map +1 -1
  5. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  6. package/dist/agents/SupervisorAgent.js +5 -0
  7. package/dist/agents/SupervisorAgent.js.map +1 -1
  8. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  9. package/dist/bridge/sse/mapper.js +1 -0
  10. package/dist/bridge/sse/mapper.js.map +1 -1
  11. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  12. package/dist/manager/agent/lifecycle.js +41 -0
  13. package/dist/manager/agent/lifecycle.js.map +1 -1
  14. package/dist/runtime/query/events.d.ts.map +1 -1
  15. package/dist/runtime/query/events.js +1 -0
  16. package/dist/runtime/query/events.js.map +1 -1
  17. package/dist/runtime/query/index.d.ts +15 -0
  18. package/dist/runtime/query/index.d.ts.map +1 -1
  19. package/dist/runtime/query/index.js +1 -0
  20. package/dist/runtime/query/index.js.map +1 -1
  21. package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
  22. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  23. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  24. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  25. package/dist/runtime/query/iteration/phases/tool-review.js +11 -1
  26. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  27. package/dist/tools/task/create.d.ts.map +1 -1
  28. package/dist/tools/task/create.js +4 -1
  29. package/dist/tools/task/create.js.map +1 -1
  30. package/dist/tools/task/list.d.ts.map +1 -1
  31. package/dist/tools/task/list.js +4 -1
  32. package/dist/tools/task/list.js.map +1 -1
  33. package/dist/tools/task/present.d.ts +18 -0
  34. package/dist/tools/task/present.d.ts.map +1 -0
  35. package/dist/tools/task/present.js +88 -0
  36. package/dist/tools/task/present.js.map +1 -0
  37. package/dist/tools/task/update.d.ts.map +1 -1
  38. package/dist/tools/task/update.js +9 -1
  39. package/dist/tools/task/update.js.map +1 -1
  40. package/dist/turn/reporter.js +1 -1
  41. package/dist/turn/reporter.js.map +1 -1
  42. package/dist/types/agent/base.d.ts +25 -0
  43. package/dist/types/agent/base.d.ts.map +1 -1
  44. package/dist/types/agent/task.d.ts +14 -0
  45. package/dist/types/agent/task.d.ts.map +1 -1
  46. package/dist/types/session/events.d.ts +8 -0
  47. package/dist/types/session/events.d.ts.map +1 -1
  48. package/dist/types/session/events.js.map +1 -1
  49. package/dist/utils/log/redact.js +1 -1
  50. package/dist/utils/log/redact.js.map +1 -1
  51. package/dist/utils/log/sinks.d.ts +1 -1
  52. package/dist/utils/log/sinks.js +2 -2
  53. package/dist/utils/log/sinks.js.map +1 -1
  54. package/package.json +1 -1
  55. package/src/agents/ReactiveAgent.ts +4 -0
  56. package/src/agents/SupervisorAgent.ts +5 -0
  57. package/src/bridge/sse/mapper.ts +1 -0
  58. package/src/manager/agent/lifecycle.ts +46 -0
  59. package/src/runtime/query/events.ts +1 -0
  60. package/src/runtime/query/index.ts +17 -0
  61. package/src/runtime/query/iteration/phases/context.ts +2 -0
  62. package/src/runtime/query/iteration/phases/tool-review.ts +12 -1
  63. package/src/tools/task/create.ts +4 -1
  64. package/src/tools/task/list.ts +4 -1
  65. package/src/tools/task/present.ts +102 -0
  66. package/src/tools/task/update.ts +9 -1
  67. package/src/turn/reporter.ts +1 -1
  68. package/src/types/agent/base.ts +26 -0
  69. package/src/types/agent/task.ts +15 -0
  70. package/src/types/session/events.ts +8 -0
  71. package/src/utils/log/redact.ts +1 -1
  72. package/src/utils/log/sinks.ts +2 -2
@@ -180,6 +180,26 @@ function mergeEnv(
180
180
  return { ...base, ...override }
181
181
  }
182
182
 
183
+ /**
184
+ * One `reviewAllowedCalls` that answers `true` when any of `sources` does.
185
+ *
186
+ * Review is only ever added along a delegation: a child's own answer joins
187
+ * its parent's, it never replaces it. Each source is read at every call, so
188
+ * a parent whose answer changes mid-turn (a mode the operator entered) is
189
+ * seen by a child already running. The same function twice (a builder that
190
+ * forwarded the override it was handed) is consulted once. Returns
191
+ * `undefined` when no source is set, and the one source unwrapped when there
192
+ * is exactly one.
193
+ */
194
+ function anyReviewAllowedCalls(
195
+ ...sources: ReadonlyArray<(() => boolean) | undefined>
196
+ ): (() => boolean) | undefined {
197
+ const present = [...new Set(sources.filter((s): s is () => boolean => s !== undefined))]
198
+ if (present.length === 0) return undefined
199
+ if (present.length === 1) return present[0]
200
+ return () => present.some((source) => source() === true)
201
+ }
202
+
183
203
  export class AgentManager {
184
204
  private registry: AgentRegistry
185
205
  private instances: Map<TaskId, AgentTask> = new Map()
@@ -542,6 +562,16 @@ export class AgentManager {
542
562
  const ownDenies = options.toolScope?.deny ?? []
543
563
  const resolvedDenies = [...new Set([...inheritedDenies, ...ownDenies])]
544
564
 
565
+ // The parent's "review even what the rules allow" (plan mode), and
566
+ // this spawn's own if it names one. OR-ed rather than replaced, for
567
+ // the same reason the denies above are a union: a descendant may ask
568
+ // for more review and never for less. The builder's value joins
569
+ // below, once the builder has run.
570
+ const inheritedReview = anyReviewAllowedCalls(
571
+ context.reviewAllowedCalls,
572
+ options.configOverrides?.reviewAllowedCalls,
573
+ )
574
+
545
575
  const childContext: AgentTaskContext = {
546
576
  parentSessionId: context.parentSessionId,
547
577
  parentTurnId: context.parentTurnId,
@@ -557,6 +587,7 @@ export class AgentManager {
557
587
  parentActor: childParentActor,
558
588
  ...(resolvedDenies.length > 0 ? { toolDenies: resolvedDenies } : {}),
559
589
  ...(context.childStorage ? { childStorage: context.childStorage } : {}),
590
+ ...(inheritedReview ? { reviewAllowedCalls: inheritedReview } : {}),
560
591
  }
561
592
 
562
593
  agentTask = Object.assign(queuedTask ?? {}, {
@@ -759,6 +790,21 @@ export class AgentManager {
759
790
  childConfig.toolResultGuardrails = inheritedScreens
760
791
  }
761
792
 
793
+ // Stamped after both branches, beside the screens and for the same
794
+ // reason: a `configBuilder` cannot forward a field it was never told
795
+ // about, and the bare-config branch builds its config by hand.
796
+ //
797
+ // Without it a child borrowed its parent's review handler and not
798
+ // the switch that sends rule-allowed and grant-covered batches to
799
+ // it, so plan mode refused the parent's next change and let the
800
+ // child's run: a call an approval earlier in the CHILD's turn
801
+ // covered never reached the handler that knew about plan mode. The
802
+ // function is carried, not sampled, so a mode entered while the
803
+ // child runs reaches its next batch. The builder's own value is
804
+ // kept and OR-ed in: it may add review, never remove the parent's.
805
+ const effectiveReview = anyReviewAllowedCalls(inheritedReview, childConfig.reviewAllowedCalls)
806
+ if (effectiveReview) childConfig.reviewAllowedCalls = effectiveReview
807
+
762
808
  // Lineage is assigned by the spawning manager, not proposed by the
763
809
  // child definition. A fixed configBuilder can ignore its inputs and
764
810
  // configOverrides is caller-authored; neither may turn a child back
@@ -283,6 +283,7 @@ export class EventTranslator {
283
283
  status: task.status,
284
284
  owner: task.owner,
285
285
  ...(task.blockedBy.length > 0 ? { blockedBy: task.blockedBy } : {}),
286
+ ...(event.type === 'task.deleted' ? { deleted: true as const } : {}),
286
287
  })
287
288
  break
288
289
  default: {
@@ -739,6 +739,22 @@ export interface QueryParams {
739
739
  current: import('../../types/permission/index.js').PermissionMode
740
740
  }
741
741
 
742
+ /**
743
+ * Whether a batch that needs no review still goes to `resumeHandler`.
744
+ *
745
+ * A batch every call of which an authorization rule allows, or which a
746
+ * grant from earlier in the turn covers, runs without asking the handler.
747
+ * That is right while the handler would approve it anyway, and wrong under
748
+ * a policy stricter than the rules: a read-only mode such as `plan`, which
749
+ * refuses a change a rule would let through. Consulted once per batch,
750
+ * just before either shortcut; `true` sends the batch to the handler, each
751
+ * call carrying the gate's decision in `authorization`. A rule's `deny`
752
+ * still refuses a call whatever this returns.
753
+ *
754
+ * Absent, both shortcuts apply as they always have.
755
+ */
756
+ reviewAllowedCalls?: () => boolean
757
+
742
758
  /**
743
759
  * A name for the policy `resumeHandler` implements.
744
760
  *
@@ -1480,6 +1496,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
1480
1496
  // Turn-scoped. An approval is a statement about this turn's work;
1481
1497
  // carrying one into a later turn would be reuse nobody agreed to.
1482
1498
  toolGrants: new ToolGrantSet(),
1499
+ ...(params.reviewAllowedCalls ? { reviewAllowedCalls: params.reviewAllowedCalls } : {}),
1483
1500
  // Turn-scoped for the same reason. A repeat count carried into a later
1484
1501
  // run is a claim about work nobody repeated, and a module-level map
1485
1502
  // would leak exactly that way.
@@ -166,6 +166,8 @@ export interface IterationContext {
166
166
  * asked about again. Absent on paths that do not review tools.
167
167
  */
168
168
  readonly toolGrants?: ToolGrantSet
169
+ /** See `QueryParams.reviewAllowedCalls`. Absent: allowed and granted batches skip review. */
170
+ readonly reviewAllowedCalls?: () => boolean
169
171
  /**
170
172
  * Absent when the host opted out with `repeatCallAdvisory: false`. The
171
173
  * opt-out is the ABSENCE, not a flag read at every call site, so a code
@@ -226,6 +226,16 @@ export async function* runToolReview(
226
226
  // must not be able to release them.
227
227
  const gateDenied = new Map<string, string>()
228
228
 
229
+ // Sampled once, the first time a shortcut asks, so both shortcuts below
230
+ // read the same answer for this batch. A policy stricter than the rules
231
+ // (a read-only mode) sees the batch rather than letting an allowance or a
232
+ // grant run it past the handler.
233
+ let reviewAllowedSample: boolean | undefined
234
+ const reviewAllowed = (): boolean => {
235
+ reviewAllowedSample ??= ctx.reviewAllowedCalls?.() === true
236
+ return reviewAllowedSample
237
+ }
238
+
229
239
  // The operator's policy runs FIRST, and a grant cannot overrule it.
230
240
  //
231
241
  // The grant short-circuit used to sit above this block and return, so a
@@ -289,7 +299,7 @@ export async function* runToolReview(
289
299
  const allAllowed = gateResults.every((gr) => gr.gateResult.decision === 'allow')
290
300
  const allDenied = gateResults.every((gr) => gr.gateResult.decision === 'deny')
291
301
 
292
- if (allAllowed) {
302
+ if (allAllowed && !reviewAllowed()) {
293
303
  ctx.log.debug('Authorization gate: all tool calls pre-approved', {
294
304
  'namzu.tool.names': gateResults.map((gr) => gr.toolCall.name),
295
305
  })
@@ -334,6 +344,7 @@ export async function* runToolReview(
334
344
  // working directory or a run outside the sandbox.
335
345
  if (
336
346
  ctx.toolGrants &&
347
+ !reviewAllowed() &&
337
348
  gateDenied.size === 0 &&
338
349
  escalated().length === 0 &&
339
350
  toolCallSummaries.every((tc) => ctx.toolGrants?.covers(tc))
@@ -4,6 +4,7 @@ import type { ToolDefinition } from '../../types/tool/index.js'
4
4
  import { asTaskId } from '../../utils/id.js'
5
5
  import { defineTool } from '../defineTool.js'
6
6
  import type { TaskToolScope } from './index.js'
7
+ import { presentTaskCreateCall, presentTaskCreateResult } from './present.js'
7
8
 
8
9
  export function buildTaskCreateTool(
9
10
  taskStore: TaskStore,
@@ -35,6 +36,8 @@ export function buildTaskCreateTool(
35
36
  readOnly: false,
36
37
  destructive: false,
37
38
  concurrencySafe: true,
39
+ presentCall: presentTaskCreateCall,
40
+ presentResult: presentTaskCreateResult,
38
41
  async execute({ subject, description, activeForm, owner, blockedBy, metadata }) {
39
42
  const task = await taskStore.create({
40
43
  sessionId: scope.sessionId,
@@ -50,7 +53,7 @@ export function buildTaskCreateTool(
50
53
  return {
51
54
  success: true,
52
55
  output: `Task created: ${task.id} — "${subject}"${owner ? ` [owner: ${owner}]` : ''}`,
53
- data: { id: task.id, status: task.status, owner: task.owner },
56
+ data: { id: task.id, subject: task.subject, status: task.status, owner: task.owner },
54
57
  }
55
58
  },
56
59
  })
@@ -4,6 +4,7 @@ import { type TaskStore, isTerminalTaskStatus } from '../../types/task/index.js'
4
4
  import type { ToolDefinition } from '../../types/tool/index.js'
5
5
  import { defineTool } from '../defineTool.js'
6
6
  import type { TaskToolScope } from './index.js'
7
+ import { countTasks, presentTaskListCall, presentTaskListResult } from './present.js'
7
8
 
8
9
  export function buildTaskListTool(
9
10
  taskStore: TaskStore,
@@ -19,6 +20,8 @@ export function buildTaskListTool(
19
20
  readOnly: true,
20
21
  destructive: false,
21
22
  concurrencySafe: true,
23
+ presentCall: presentTaskListCall,
24
+ presentResult: presentTaskListResult,
22
25
  async execute() {
23
26
  const all = await taskStore.list({ sessionId: scope.sessionId })
24
27
  // Blockers resolve against every task of the session, shown or not: a
@@ -53,7 +56,7 @@ export function buildTaskListTool(
53
56
  output:
54
57
  tasks.length === 0
55
58
  ? 'No planning tasks yet. This list does not report delegated agent status; use agent_task_list when available.'
56
- : `${stats.total} tasks: ${stats.completed} completed, ${stats.in_progress} in progress, ${stats.pending} pending.`,
59
+ : `${countTasks(stats.total)}: ${stats.completed} completed, ${stats.in_progress} in progress, ${stats.pending} pending.`,
57
60
  data: { tasks: summary, stats },
58
61
  }
59
62
  },
@@ -0,0 +1,102 @@
1
+ import type { ToolResult } from '../../types/tool/index.js'
2
+ import type { ToolCallView, ToolResultView } from '../../types/tool/presentation.js'
3
+
4
+ /**
5
+ * How the task tools read to a person.
6
+ *
7
+ * The model-facing output of these tools names task ids, because the model
8
+ * needs them to call `task_update`. None of that is for the operator: with no
9
+ * opinion of its own a task call fell through to the generic presenter, which
10
+ * printed the arguments (`{"id":"01a0…","status":"completed"}`) as the call
11
+ * row and the model's receipt (`Task 01a0… updated — status: completed`) as
12
+ * the result. So each tool says what happened in words — `Added task ·
13
+ * Write the parser` — and a successful receipt adds nothing beneath it.
14
+ *
15
+ * Deliberately no ids, no owner and no JSON anywhere in these views. A host
16
+ * that wants the ids still has the tool result and its `data`.
17
+ */
18
+
19
+ const MAX_SUBJECT = 100
20
+
21
+ function oneLine(value: string): string {
22
+ const flat = value.replace(/\s+/g, ' ').trim()
23
+ return flat.length > MAX_SUBJECT ? `${flat.slice(0, MAX_SUBJECT - 1)}…` : flat
24
+ }
25
+
26
+ function activity(label: string): ToolCallView {
27
+ return { kind: 'generic', presentation: 'activity', label }
28
+ }
29
+
30
+ /** A successful receipt: the call row already said it. */
31
+ const HIDDEN_RESULT: ToolResultView = { kind: 'generic', label: '', visibility: 'hidden' }
32
+
33
+ /** `N task` / `N tasks`. */
34
+ export function countTasks(count: number): string {
35
+ return `${count} task${count === 1 ? '' : 's'}`
36
+ }
37
+
38
+ /** What a failed task call says, without the id the model passed. */
39
+ function failure(result: ToolResult, fallback: string): ToolResultView {
40
+ const text = `${result.error ?? result.output ?? ''}`
41
+ return {
42
+ kind: 'generic',
43
+ label: /not found/i.test(text) ? 'No task has that id' : fallback,
44
+ }
45
+ }
46
+
47
+ export function presentTaskCreateCall(input: { readonly subject?: unknown }): ToolCallView {
48
+ const subject = typeof input.subject === 'string' ? oneLine(input.subject) : ''
49
+ return activity(subject ? `Add task · ${subject}` : 'Add task')
50
+ }
51
+
52
+ export function presentTaskCreateResult(_input: unknown, result: ToolResult): ToolResultView {
53
+ if (!result.success) return failure(result, 'The task was not added')
54
+ return HIDDEN_RESULT
55
+ }
56
+
57
+ /** The verb for a `task_update` call, from the status it asks for. */
58
+ export function taskUpdateVerb(status: unknown): string {
59
+ switch (status) {
60
+ case 'in_progress':
61
+ return 'Start task'
62
+ case 'completed':
63
+ return 'Complete task'
64
+ case 'deleted':
65
+ return 'Remove task'
66
+ case 'pending':
67
+ return 'Reopen task'
68
+ default:
69
+ return 'Update task'
70
+ }
71
+ }
72
+
73
+ export function presentTaskUpdateCall(input: {
74
+ readonly status?: unknown
75
+ readonly subject?: unknown
76
+ }): ToolCallView {
77
+ const verb = taskUpdateVerb(input.status)
78
+ const subject = typeof input.subject === 'string' ? oneLine(input.subject) : ''
79
+ return activity(subject ? `${verb} · ${subject}` : verb)
80
+ }
81
+
82
+ export function presentTaskUpdateResult(_input: unknown, result: ToolResult): ToolResultView {
83
+ if (!result.success) return failure(result, 'The task was not changed')
84
+ return HIDDEN_RESULT
85
+ }
86
+
87
+ export function presentTaskListCall(): ToolCallView {
88
+ return activity('Check tasks')
89
+ }
90
+
91
+ export function presentTaskListResult(_input: unknown, result: ToolResult): ToolResultView {
92
+ if (!result.success) return { kind: 'generic', label: 'The task list could not be read' }
93
+ const stats = (result.data as { stats?: { total?: unknown; completed?: unknown } } | undefined)
94
+ ?.stats
95
+ const total = typeof stats?.total === 'number' ? stats.total : undefined
96
+ const completed = typeof stats?.completed === 'number' ? stats.completed : undefined
97
+ if (total === undefined || completed === undefined) return HIDDEN_RESULT
98
+ return {
99
+ kind: 'generic',
100
+ label: total === 0 ? 'No tasks yet' : `Tasks · ${completed}/${total} done`,
101
+ }
102
+ }
@@ -3,6 +3,7 @@ import type { TaskStore } from '../../types/task/index.js'
3
3
  import type { ToolDefinition } from '../../types/tool/index.js'
4
4
  import { asTaskId } from '../../utils/id.js'
5
5
  import { defineTool } from '../defineTool.js'
6
+ import { presentTaskUpdateCall, presentTaskUpdateResult } from './present.js'
6
7
 
7
8
  export function buildTaskUpdateTool(taskStore: TaskStore): ToolDefinition {
8
9
  return defineTool({
@@ -34,6 +35,8 @@ export function buildTaskUpdateTool(taskStore: TaskStore): ToolDefinition {
34
35
  readOnly: false,
35
36
  destructive: false,
36
37
  concurrencySafe: true,
38
+ presentCall: presentTaskUpdateCall,
39
+ presentResult: presentTaskUpdateResult,
37
40
  async execute({
38
41
  id,
39
42
  subject,
@@ -85,7 +88,12 @@ export function buildTaskUpdateTool(taskStore: TaskStore): ToolDefinition {
85
88
  return {
86
89
  success: true,
87
90
  output: `Task ${id} updated — status: ${updated.status}`,
88
- data: { id: updated.id, status: updated.status, owner: updated.owner },
91
+ data: {
92
+ id: updated.id,
93
+ subject: updated.subject,
94
+ status: updated.status,
95
+ owner: updated.owner,
96
+ },
89
97
  }
90
98
  },
91
99
  })
@@ -170,7 +170,7 @@ export function createTurnReporter(parentLogger?: Logger): TurnReporter {
170
170
  break
171
171
 
172
172
  case 'task_updated':
173
- log.info('Task updated', {
173
+ log.info(event.deleted ? 'Task removed' : 'Task updated', {
174
174
  'namzu.task.subject': event.subject,
175
175
  [NAMZU.TURN_ID]: event.turnId,
176
176
  'namzu.task.id': event.taskId,
@@ -293,6 +293,32 @@ export interface BaseAgentConfig {
293
293
  * host that never wired one is unaffected.
294
294
  */
295
295
  resumeHandler?: ResumeHandler
296
+
297
+ /**
298
+ * Whether a batch that needs no review still goes to `resumeHandler`, in
299
+ * this agent's turn and in every turn it delegates to. See
300
+ * {@link import('../../runtime/query/index.js').QueryParams.reviewAllowedCalls}.
301
+ *
302
+ * A delegated child borrows its parent's handler, and a handler that
303
+ * refuses a change in a read-only mode (`plan`) cannot refuse a batch that
304
+ * never reaches it: one a rule allows, or one an approval given earlier in
305
+ * the CHILD's turn covers. So `AgentManager` stamps the spawning context's
306
+ * function (`AgentTaskContext.reviewAllowedCalls`) onto the child config
307
+ * after the builder returns, the same way it stamps the handler.
308
+ *
309
+ * Passed as a function and read once per batch, so a mode the operator
310
+ * enters while a child is already running reaches that child's next batch.
311
+ *
312
+ * **It only ever adds review.** A value the child config sets itself (from
313
+ * its `configBuilder` or `configOverrides`) is kept, and consulted together
314
+ * with the inherited one: the child's batch goes to review when EITHER
315
+ * says so. A child can ask for more review than its parent; it cannot
316
+ * answer `false` over a parent that answers `true`.
317
+ *
318
+ * Absent, and nothing inherited: rule-allowed and grant-covered batches run
319
+ * without asking, as they always have.
320
+ */
321
+ reviewAllowedCalls?: () => boolean
296
322
  }
297
323
 
298
324
  export type RuntimeToolOverrides = Record<string, ToolAvailability | 'disabled'>
@@ -142,6 +142,21 @@ export interface AgentTaskContext {
142
142
  */
143
143
  readonly childStorage?: ChildSessionStorage
144
144
 
145
+ /**
146
+ * The parent's `reviewAllowedCalls`, handed to every child it delegates
147
+ * to and on to theirs. See `BaseAgentConfig.reviewAllowedCalls`.
148
+ *
149
+ * The function itself, not a sampled value, so a stricter mode entered
150
+ * while a child runs reaches that child's next batch. `AgentManager`
151
+ * stamps it onto the child config after the builder runs and carries it
152
+ * on the child's own context; a child config that sets its own keeps it,
153
+ * and the two are OR-ed, so a descendant can add review and never shed
154
+ * it. `SupervisorAgent` sets it from its config; a host that builds its
155
+ * own context for a `TaskScheduler` sets it from the same function it
156
+ * passes its own `query()`. Absent: children decide from their own config.
157
+ */
158
+ readonly reviewAllowedCalls?: () => boolean
159
+
145
160
  /** Isolation boundary. Required per session-hierarchy.md §12.1. */
146
161
  tenantId: TenantId
147
162
 
@@ -944,6 +944,14 @@ type CoreSessionEvent =
944
944
  owner?: string
945
945
  /** See `task_created`. Carried on updates because an edge can be added later. */
946
946
  blockedBy?: readonly TaskId[]
947
+ /**
948
+ * The task was removed from the list (`task_update` with status
949
+ * `deleted`). `status` and `subject` are what it had when it went.
950
+ * Absent on every other update: without it a removal reached a
951
+ * reader as an update that changed nothing, so a checklist kept
952
+ * drawing the task as open.
953
+ */
954
+ deleted?: true
947
955
  }
948
956
  | {
949
957
  type: 'plugin_hook_executing'
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Runs once, in the pipeline, ahead of sink dispatch — not inside
4
4
  // jsonLinesSink, and not inside any other individual sink. A scan that only
5
- // jsonLinesSink ran would leave prettySink (the one `namzu run` actually
5
+ // jsonLinesSink ran would leave prettySink (the one `namzu exec` actually
6
6
  // uses) and every host-supplied sink with no second layer at all, which is
7
7
  // the gap a scoped-to-one-sink design would reintroduce: the machine-read
8
8
  // path would be the only protected one.
@@ -19,7 +19,7 @@ export const NOOP_SINK: LogSink = {
19
19
 
20
20
  /**
21
21
  * One JSON object per line — the canonical wire format for the machine-read
22
- * path (`namzu run-stream`'s stderr). JSON string-escaping neutralises
22
+ * path (`namzu exec --json`'s stderr). JSON string-escaping neutralises
23
23
  * `\n`/`\r` by construction, closing the log-forging surface without a
24
24
  * single character-stripping call site, which is bypassable anyway.
25
25
  *
@@ -67,7 +67,7 @@ const SEVERITY_LABEL: Record<LogRecord['severityText'], string> = {
67
67
  // guarantee yet that either is a constant (that lands with the CI gate in
68
68
  // later work) — a remote MCP server that names itself an escape sequence
69
69
  // erasing the previous line and printing a fake refusal is a real forging
70
- // attempt against the terminal `namzu run` writes to today, not a
70
+ // attempt against the terminal `namzu exec` writes to today, not a
71
71
  // hypothetical one.
72
72
  // biome-ignore lint/suspicious/noControlCharactersInRegex: this pattern IS the control-character filter — the escapes below are ASCII text (`\x00`-`\x1F`, `\x7F`), not raw bytes pasted into the source.
73
73
  const CONTROL_BYTE = /[\x00-\x1F\x7F]/g