@yeaft/webchat-agent 1.0.411 → 1.0.413

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.
@@ -2,7 +2,8 @@ import { DatabaseSync } from 'node:sqlite';
2
2
  import { mkdirSync, realpathSync } from 'node:fs';
3
3
  import { dirname, resolve } from 'node:path';
4
4
  import { createHash, randomUUID } from 'node:crypto';
5
- import { normalizeEvidence } from './evidence.js';
5
+ import { normalizeEvidence, normalizeOutputs } from './evidence.js';
6
+ import { normalizeContractPatch } from './completion-contract.js';
6
7
  import { normalizeActionCheckpoint } from './action-checkpoint.js';
7
8
  import { currentActionInputEventIds, runMatchesActionIdentity } from './action-identity.js';
8
9
  import { isDynamicWorkItem, usesLegacyGraph } from './execution-mode.js';
@@ -108,6 +109,7 @@ function mapWorkItem(row) {
108
109
  ledgerRevision: Math.max(0, Number(row.ledger_revision) || 0),
109
110
  coordinationMode: row.coordination_mode || 'legacy',
110
111
  finalResult: parseJson(row.final_result, null),
112
+ deliveryTarget: row.delivery_target || null,
111
113
  title: row.title,
112
114
  goal: row.goal,
113
115
  acceptanceCriteria: parseJson(row.acceptance_criteria, []),
@@ -211,6 +213,7 @@ function mapAction(row) {
211
213
  modelPolicy: parseJson(row.model_policy, null),
212
214
  dependsOnStageIds: parseJson(row.depends_on_stage_ids, []),
213
215
  sourceActionIds: parseJson(row.source_action_ids, []),
216
+ creationSource: row.creation_source || 'legacy',
214
217
  workspaceMode: row.workspace_mode || 'shared',
215
218
  changesRequestedStageId: row.changes_requested_stage_id || null,
216
219
  workspace: parseJson(row.workspace, null),
@@ -229,6 +232,8 @@ function mapAction(row) {
229
232
  currentRunId: row.current_run_id || null,
230
233
  leaseEpoch: row.lease_epoch,
231
234
  replacesActionId: row.replaces_action_id || null,
235
+ closeReason: row.close_reason || null,
236
+ closedAt: row.closed_at || null,
232
237
  createdAt: row.created_at,
233
238
  updatedAt: row.updated_at,
234
239
  };
@@ -271,6 +276,7 @@ function mapRun(row) {
271
276
  response: row.response || '',
272
277
  summary: row.summary || '',
273
278
  evidence: normalizeEvidence(parseJson(row.evidence, [])),
279
+ outputs: normalizeOutputs(parseJson(row.outputs, [])),
274
280
  acceptanceChecks: parseJson(row.acceptance_checks, []),
275
281
  waitingReason: row.waiting_reason || null,
276
282
  error: row.error || null,
@@ -830,6 +836,7 @@ export class WorkItemStore {
830
836
  ledger_revision INTEGER NOT NULL DEFAULT 0,
831
837
  coordination_mode TEXT NOT NULL DEFAULT 'legacy',
832
838
  final_result TEXT,
839
+ delivery_target TEXT,
833
840
  title TEXT NOT NULL,
834
841
  goal TEXT NOT NULL,
835
842
  acceptance_criteria TEXT NOT NULL,
@@ -861,6 +868,7 @@ export class WorkItemStore {
861
868
  model_policy TEXT,
862
869
  depends_on_stage_ids TEXT NOT NULL DEFAULT '[]',
863
870
  source_action_ids TEXT NOT NULL DEFAULT '[]',
871
+ creation_source TEXT NOT NULL DEFAULT 'legacy',
864
872
  workspace_mode TEXT NOT NULL DEFAULT 'shared',
865
873
  changes_requested_stage_id TEXT,
866
874
  workspace TEXT,
@@ -878,6 +886,8 @@ export class WorkItemStore {
878
886
  current_run_id TEXT,
879
887
  lease_epoch INTEGER NOT NULL DEFAULT 0,
880
888
  replaces_action_id TEXT REFERENCES actions(id) ON DELETE SET NULL,
889
+ close_reason TEXT,
890
+ closed_at INTEGER,
881
891
  created_at INTEGER NOT NULL,
882
892
  updated_at INTEGER NOT NULL,
883
893
  UNIQUE(work_item_id, sequence)
@@ -900,6 +910,7 @@ export class WorkItemStore {
900
910
  execution_manifest TEXT,
901
911
  summary TEXT,
902
912
  evidence TEXT NOT NULL DEFAULT '[]',
913
+ outputs TEXT NOT NULL DEFAULT '[]',
903
914
  acceptance_checks TEXT NOT NULL DEFAULT '[]',
904
915
  waiting_reason TEXT,
905
916
  error TEXT,
@@ -1106,9 +1117,15 @@ export class WorkItemStore {
1106
1117
  if (!hasColumn(this.db, 'work_items', 'final_result')) {
1107
1118
  this.db.exec('ALTER TABLE work_items ADD COLUMN final_result TEXT');
1108
1119
  }
1120
+ if (!hasColumn(this.db, 'work_items', 'delivery_target')) {
1121
+ this.db.exec('ALTER TABLE work_items ADD COLUMN delivery_target TEXT');
1122
+ }
1109
1123
  if (!hasColumn(this.db, 'actions', 'source_action_ids')) {
1110
1124
  this.db.exec("ALTER TABLE actions ADD COLUMN source_action_ids TEXT NOT NULL DEFAULT '[]'");
1111
1125
  }
1126
+ if (!hasColumn(this.db, 'actions', 'creation_source')) {
1127
+ this.db.exec("ALTER TABLE actions ADD COLUMN creation_source TEXT NOT NULL DEFAULT 'legacy'");
1128
+ }
1112
1129
  if (!hasColumn(this.db, 'actions', 'generation')) {
1113
1130
  this.db.exec('ALTER TABLE actions ADD COLUMN generation INTEGER NOT NULL DEFAULT 1');
1114
1131
  }
@@ -1131,6 +1148,15 @@ export class WorkItemStore {
1131
1148
  if (!hasColumn(this.db, 'actions', 'replaces_action_id')) {
1132
1149
  this.db.exec('ALTER TABLE actions ADD COLUMN replaces_action_id TEXT REFERENCES actions(id) ON DELETE SET NULL');
1133
1150
  }
1151
+ if (!hasColumn(this.db, 'actions', 'close_reason')) {
1152
+ this.db.exec('ALTER TABLE actions ADD COLUMN close_reason TEXT');
1153
+ }
1154
+ if (!hasColumn(this.db, 'actions', 'closed_at')) {
1155
+ this.db.exec('ALTER TABLE actions ADD COLUMN closed_at INTEGER');
1156
+ }
1157
+ if (!hasColumn(this.db, 'runs', 'outputs')) {
1158
+ this.db.exec("ALTER TABLE runs ADD COLUMN outputs TEXT NOT NULL DEFAULT '[]'");
1159
+ }
1134
1160
  if (!hasColumn(this.db, 'runs', 'action_generation')) {
1135
1161
  this.db.exec('ALTER TABLE runs ADD COLUMN action_generation INTEGER NOT NULL DEFAULT 1');
1136
1162
  }
@@ -2296,14 +2322,15 @@ export class WorkItemStore {
2296
2322
  const id = input.id || randomUUID();
2297
2323
  const workspaceKey = canonicalWorkspaceKey(input.workDir);
2298
2324
  this.db.prepare(`INSERT INTO work_items
2299
- (id, revision, execution_schema_version, ledger_revision, coordination_mode, final_result,
2325
+ (id, revision, execution_schema_version, ledger_revision, coordination_mode, final_result, delivery_target,
2300
2326
  title, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
2301
2327
  current_action_id, current_run_id, work_dir, workspace_key, reuse_memory, origin, linked_session_ids,
2302
2328
  session_context, attachments, created_at, updated_at)
2303
- VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
2329
+ VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
2304
2330
  id,
2305
2331
  Number.isInteger(input.executionSchemaVersion) ? input.executionSchemaVersion : 2,
2306
2332
  input.coordinationMode || 'legacy',
2333
+ input.deliveryTarget || null,
2307
2334
  input.title,
2308
2335
  input.goal,
2309
2336
  stringify(input.acceptanceCriteria || []),
@@ -2330,15 +2357,26 @@ export class WorkItemStore {
2330
2357
  });
2331
2358
  }
2332
2359
 
2333
- #insertAction(workItemId, input, sequence, now = this.now()) {
2360
+ #insertAction(workItemId, input, sequence, now = this.now(), options = {}) {
2361
+ const workItem = this.getWorkItem(workItemId);
2334
2362
  const stageId = input.stageId || input.type;
2335
- const activeStage = usesLegacyGraph(this.getWorkItem(workItemId))
2363
+ const activeStage = usesLegacyGraph(workItem)
2336
2364
  ? this.db.prepare(`SELECT id FROM actions WHERE work_item_id = ? AND stage_id = ?
2337
2365
  AND status NOT IN ('superseded', 'cancelled') LIMIT 1`).get(workItemId, stageId)
2338
2366
  : null;
2339
2367
  if (activeStage) {
2340
2368
  throw new Error(`Work Center Action stage identity is already active: ${stageId}`);
2341
2369
  }
2370
+ const creationSource = input.creationSource || 'legacy';
2371
+ if (input.type === 'create_vp'
2372
+ && (!isDynamicWorkItem(workItem)
2373
+ || creationSource !== 'dynamic_coordinator'
2374
+ || options.dynamicCoordinator !== true)) {
2375
+ throw new Error('create_vp Actions can only be persisted by the dynamic WorkItem Coordinator');
2376
+ }
2377
+ if (input.type === 'create_vp' && input.workspaceMode === 'read') {
2378
+ throw new Error('create_vp Actions cannot use read workspace mode because VP creation mutates Agent-global state');
2379
+ }
2342
2380
  const action = {
2343
2381
  id: input.id || randomUUID(),
2344
2382
  workItemId,
@@ -2349,6 +2387,7 @@ export class WorkItemStore {
2349
2387
  modelPolicy: input.modelPolicy || null,
2350
2388
  dependsOnStageIds: Array.isArray(input.dependsOnStageIds) ? input.dependsOnStageIds : [],
2351
2389
  sourceActionIds: Array.isArray(input.sourceActionIds) ? input.sourceActionIds : [],
2390
+ creationSource,
2352
2391
  workspaceMode: input.workspaceMode || 'shared',
2353
2392
  changesRequestedStageId: input.changesRequestedStageId || null,
2354
2393
  workspace: input.workspace || null,
@@ -2373,10 +2412,10 @@ export class WorkItemStore {
2373
2412
  action.identityHistory = actionIdentityHistory(action);
2374
2413
  this.db.prepare(`INSERT INTO actions
2375
2414
  (id, work_item_id, sequence, type, required_role, stage_id, assignment_policy, model_policy,
2376
- depends_on_stage_ids, source_action_ids, workspace_mode, changes_requested_stage_id, workspace,
2415
+ depends_on_stage_ids, source_action_ids, creation_source, workspace_mode, changes_requested_stage_id, workspace,
2377
2416
  instruction, brief, context, contract_revision, generation, spec_hash, identity_history, result_run_id,
2378
2417
  status, attempt, max_attempts, current_run_id, lease_epoch, replaces_action_id, created_at, updated_at)
2379
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?, ?)`).run(
2418
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?, ?)`).run(
2380
2419
  action.id,
2381
2420
  workItemId,
2382
2421
  action.sequence,
@@ -2387,6 +2426,7 @@ export class WorkItemStore {
2387
2426
  stringify(action.modelPolicy),
2388
2427
  stringify(action.dependsOnStageIds),
2389
2428
  stringify(action.sourceActionIds),
2429
+ action.creationSource,
2390
2430
  action.workspaceMode,
2391
2431
  action.changesRequestedStageId,
2392
2432
  stringify(action.workspace),
@@ -3150,10 +3190,11 @@ export class WorkItemStore {
3150
3190
  coordinatorRevision: detail.coordinatorRevision,
3151
3191
  status: detail.status,
3152
3192
  actionFence: coordinatorActionFence(
3153
- (detail.actions || []).filter(action => !['completed', 'superseded', 'cancelled'].includes(action.status)),
3193
+ (detail.actions || []).filter(action => !['completed', 'closed', 'superseded', 'cancelled'].includes(action.status)),
3154
3194
  ),
3155
3195
  recovery: assistant.recovery ? { ...assistant.recovery } : null,
3156
3196
  automatic: assistant.automatic === true,
3197
+ userOriginated: assistant.userOriginated === true,
3157
3198
  claim: {
3158
3199
  mailboxId: claim.mailboxId,
3159
3200
  ownerBootId: claim.ownerBootId,
@@ -3215,9 +3256,10 @@ export class WorkItemStore {
3215
3256
  coordinatorRevision,
3216
3257
  status: workItem.status,
3217
3258
  actionFence: coordinatorActionFence(
3218
- (detail.actions || []).filter(action => !['completed', 'superseded', 'cancelled'].includes(action.status)),
3259
+ (detail.actions || []).filter(action => !['completed', 'closed', 'superseded', 'cancelled'].includes(action.status)),
3219
3260
  ),
3220
3261
  automatic: true,
3262
+ userOriginated: false,
3221
3263
  claim: {
3222
3264
  mailboxId: mailbox.id,
3223
3265
  ownerBootId: expected.ownerBootId,
@@ -3285,7 +3327,7 @@ export class WorkItemStore {
3285
3327
  isImage: attachment.isImage === true,
3286
3328
  }));
3287
3329
  const activeActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
3288
- AND status NOT IN ('completed', 'superseded', 'cancelled') ORDER BY sequence`).all(id).map(mapAction);
3330
+ AND status NOT IN ('completed', 'closed', 'superseded', 'cancelled') ORDER BY sequence`).all(id).map(mapAction);
3289
3331
  this.#assertNoIntegrationReservation(activeActions, now);
3290
3332
  let recovery = options.recovery && typeof options.recovery === 'object'
3291
3333
  ? { ...options.recovery } : null;
@@ -3325,6 +3367,7 @@ export class WorkItemStore {
3325
3367
  const assistantMessage = {
3326
3368
  id: randomUUID(), turnId, role: 'assistant', text: '', status: 'thinking',
3327
3369
  createdAt: now, updatedAt: now, decision: null,
3370
+ userOriginated: userMessage !== null,
3328
3371
  ...(recovery ? { recovery: { ...recovery } } : {}),
3329
3372
  };
3330
3373
  if (userMessage) {
@@ -3380,6 +3423,8 @@ export class WorkItemStore {
3380
3423
  status: workItem.status,
3381
3424
  actionFence: coordinatorActionFence(activeActions),
3382
3425
  recovery: recovery ? { ...recovery } : null,
3426
+ userOriginated: userMessage !== null,
3427
+ claim: null,
3383
3428
  },
3384
3429
  };
3385
3430
  });
@@ -3411,7 +3456,7 @@ export class WorkItemStore {
3411
3456
  const decision = result?.decision || {};
3412
3457
  const now = this.now();
3413
3458
  const activeActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
3414
- AND status NOT IN ('completed', 'superseded', 'cancelled') ORDER BY sequence`).all(workItem.id).map(mapAction);
3459
+ AND status NOT IN ('completed', 'closed', 'superseded', 'cancelled') ORDER BY sequence`).all(workItem.id).map(mapAction);
3415
3460
  if (coordinatorActionFence(activeActions) !== expected.actionFence) {
3416
3461
  throw new Error('WorkItem Actions changed while the Coordinator was responding; send the message again');
3417
3462
  }
@@ -3421,6 +3466,9 @@ export class WorkItemStore {
3421
3466
  turnId, result, expected, workItem, messages, assistantIndex, activeActions, now,
3422
3467
  });
3423
3468
  }
3469
+ if (decision.contractPatch?.deliveryTarget) {
3470
+ throw new Error('Only a dynamic user-originated Coordinator turn can confirm the WorkItem delivery target');
3471
+ }
3424
3472
 
3425
3473
  const graphMode = usesLegacyGraph(workItem);
3426
3474
  if (decision.kind === 'replan'
@@ -3636,10 +3684,33 @@ export class WorkItemStore {
3636
3684
  turnId, result, expected, workItem, messages, assistantIndex, activeActions, now,
3637
3685
  }) {
3638
3686
  const decision = result?.decision || {};
3687
+ if (expected.automatic === true && result?.mutation?.contractPatch?.deliveryTarget) {
3688
+ throw new Error('Automatic Work Center Coordinator delivery target changes are forbidden');
3689
+ }
3690
+ if (decision.contractPatch?.deliveryTarget && expected.userOriginated !== true) {
3691
+ throw new Error('WorkItem delivery target confirmation requires a user-originated Coordinator turn');
3692
+ }
3693
+ if (decision.contractPatch?.deliveryTarget && decision.kind !== 'request_human') {
3694
+ throw new Error('WorkItem delivery target confirmation requires a user-originated request_human decision');
3695
+ }
3639
3696
  let affectedActionIds = [];
3640
3697
  let nextStatus = workItem.status;
3641
3698
  let currentActionId = workItem.currentActionId;
3642
3699
  let finalResult = null;
3700
+ const contractPatch = normalizeContractPatch(decision.contractPatch);
3701
+ if (decision.kind === 'create_actions'
3702
+ && !workItem.deliveryTarget
3703
+ && (result?.mutation?.createdActions || []).some(action => (
3704
+ action.type === 'create_vp' || action.workspaceMode !== 'read'
3705
+ ))) {
3706
+ throw new Error('WorkItem delivery target must be confirmed before creating mutating or delivery Actions');
3707
+ }
3708
+ if (contractPatch) {
3709
+ if (contractPatch.title) workItem.title = contractPatch.title;
3710
+ if (contractPatch.goal) workItem.goal = contractPatch.goal;
3711
+ if (contractPatch.acceptanceCriteria) workItem.acceptanceCriteria = contractPatch.acceptanceCriteria;
3712
+ if (contractPatch.deliveryTarget) workItem.deliveryTarget = contractPatch.deliveryTarget;
3713
+ }
3643
3714
 
3644
3715
  if (decision.kind === 'create_actions') {
3645
3716
  if (activeActions.some(action => action.status === 'running')) {
@@ -3649,6 +3720,24 @@ export class WorkItemStore {
3649
3720
  if (!mutation || !Array.isArray(mutation.createdActions) || mutation.createdActions.length === 0) {
3650
3721
  throw new Error('Dynamic Coordinator Action creation is missing a validated mutation');
3651
3722
  }
3723
+ for (const closure of mutation.closeActions || []) {
3724
+ const action = activeActions.find(candidate => candidate.id === closure.actionId);
3725
+ if (!action || !['waiting', 'failed'].includes(action.status)) {
3726
+ throw new Error('Dynamic Coordinator close target changed before apply');
3727
+ }
3728
+ this.#supersedePendingActionInputs([action], closure.reason, now);
3729
+ const changed = this.db.prepare(`UPDATE actions SET status = 'closed', current_run_id = NULL,
3730
+ close_reason = ?, closed_at = ?, lease_epoch = lease_epoch + 1, updated_at = ?
3731
+ WHERE id = ? AND generation = ? AND status IN ('waiting', 'failed')`).run(
3732
+ closure.reason, now, now, action.id, action.generation,
3733
+ );
3734
+ if (Number(changed.changes) !== 1) throw new Error('Dynamic Coordinator lost an Action close fence');
3735
+ affectedActionIds.push(action.id);
3736
+ this.appendEvent(workItem.id, 'action.closed', { reason: closure.reason }, {
3737
+ actionId: action.id,
3738
+ actionGeneration: action.generation,
3739
+ });
3740
+ }
3652
3741
  for (const actionId of mutation.supersedeActionIds || []) {
3653
3742
  const action = activeActions.find(candidate => candidate.id === actionId);
3654
3743
  if (!action || !['ready', 'waiting', 'failed'].includes(action.status)) {
@@ -3679,8 +3768,11 @@ export class WorkItemStore {
3679
3768
  const current = this.getWorkItem(workItem.id);
3680
3769
  for (const candidate of mutation.createdActions) {
3681
3770
  const action = this.#insertAction(workItem.id, {
3682
- ...candidate, status: 'ready', contractRevision: current.revision,
3683
- }, this.#nextSequence(workItem.id), now);
3771
+ ...candidate,
3772
+ creationSource: 'dynamic_coordinator',
3773
+ status: 'ready',
3774
+ contractRevision: current.revision,
3775
+ }, this.#nextSequence(workItem.id), now, { dynamicCoordinator: true });
3684
3776
  affectedActionIds.push(action.id);
3685
3777
  }
3686
3778
  nextStatus = 'ready';
@@ -3719,7 +3811,29 @@ export class WorkItemStore {
3719
3811
  nextStatus = 'waiting';
3720
3812
  currentActionId = null;
3721
3813
  } else if (decision.kind === 'complete') {
3722
- if (activeActions.length > 0) throw new Error('Work Center cannot complete with unfinished Actions');
3814
+ const closing = new Map((decision.closeActions || []).map(entry => [entry.actionId, entry]));
3815
+ if (activeActions.some(action => action.status !== 'closed' && !closing.has(action.id))) {
3816
+ throw new Error('Work Center cannot complete with unfinished Actions');
3817
+ }
3818
+ for (const action of activeActions) {
3819
+ const closure = closing.get(action.id);
3820
+ if (!closure) continue;
3821
+ if (!['waiting', 'failed'].includes(action.status)) {
3822
+ throw new Error('Dynamic Coordinator completion close target changed before apply');
3823
+ }
3824
+ this.#supersedePendingActionInputs([action], closure.reason, now);
3825
+ const changed = this.db.prepare(`UPDATE actions SET status = 'closed', current_run_id = NULL,
3826
+ close_reason = ?, closed_at = ?, lease_epoch = lease_epoch + 1, updated_at = ?
3827
+ WHERE id = ? AND generation = ? AND status IN ('waiting', 'failed')`).run(
3828
+ closure.reason, now, now, action.id, action.generation,
3829
+ );
3830
+ if (Number(changed.changes) !== 1) throw new Error('Dynamic Coordinator completion lost an Action close fence');
3831
+ affectedActionIds.push(action.id);
3832
+ this.appendEvent(workItem.id, 'action.closed', { reason: closure.reason }, {
3833
+ actionId: action.id,
3834
+ actionGeneration: action.generation,
3835
+ });
3836
+ }
3723
3837
  if (this.#hasBlockingOperation(workItem.id)) {
3724
3838
  throw new Error('WorkItem has an unsafe blocking Operation and cannot complete');
3725
3839
  }
@@ -3744,6 +3858,27 @@ export class WorkItemStore {
3744
3858
  throw new Error(`Completion evidence Run has incomplete acceptance checks: ${runId}`);
3745
3859
  }
3746
3860
  }
3861
+ finalResult.outputs = [];
3862
+ const seenOutputs = new Set();
3863
+ for (const runId of finalResult.evidenceRunIds) {
3864
+ for (const output of canonicalRuns.get(runId)?.outputs || []) {
3865
+ const key = `${output.kind}\u0000${output.ref}`;
3866
+ if (seenOutputs.has(key)) continue;
3867
+ seenOutputs.add(key);
3868
+ finalResult.outputs.push({ ...output, runId });
3869
+ }
3870
+ }
3871
+ const requiredOutputKind = {
3872
+ workspace_files: 'file',
3873
+ pull_request: 'pr',
3874
+ merge: 'commit',
3875
+ }[workItem.deliveryTarget];
3876
+ if (!requiredOutputKind) {
3877
+ throw new Error('WorkItem delivery target must be confirmed before completion');
3878
+ }
3879
+ if (!finalResult.outputs.some(output => output.kind === requiredOutputKind)) {
3880
+ throw new Error(`WorkItem completion requires a canonical ${requiredOutputKind} output for delivery target ${workItem.deliveryTarget}`);
3881
+ }
3747
3882
  for (const [index, acceptanceResult] of finalResult.acceptanceResults.entries()) {
3748
3883
  const provesCriterion = acceptanceResult.evidenceRunIds.some(runId => (
3749
3884
  canonicalRuns.get(runId)?.acceptanceChecks?.[index]?.criterion === criteria[index]
@@ -3762,7 +3897,12 @@ export class WorkItemStore {
3762
3897
  messages[assistantIndex] = {
3763
3898
  ...messages[assistantIndex], text: result.reply, status: 'completed', updatedAt: now,
3764
3899
  ...(result.speaker ? { speaker: result.speaker } : {}),
3765
- decision: { kind: decision.kind, reason: decision.reason, affectedActionIds },
3900
+ decision: {
3901
+ kind: decision.kind,
3902
+ reason: decision.reason,
3903
+ affectedActionIds,
3904
+ ...(decision.kind === 'request_human' ? { question: decision.question } : {}),
3905
+ },
3766
3906
  };
3767
3907
  this.#appendConversationEntry(
3768
3908
  workItem.id, messages[assistantIndex], `coordinator:turn:${turnId}:assistant`,
@@ -3774,11 +3914,13 @@ export class WorkItemStore {
3774
3914
  const current = this.getWorkItem(workItem.id);
3775
3915
  const changed = this.db.prepare(`UPDATE work_items SET messages = ?,
3776
3916
  coordinator_revision = coordinator_revision + 1, status = ?, current_action_id = ?,
3777
- current_run_id = NULL, final_result = COALESCE(final_result, ?), updated_at = ?
3917
+ current_run_id = NULL, final_result = COALESCE(final_result, ?), title = ?, goal = ?,
3918
+ acceptance_criteria = ?, delivery_target = ?, revision = revision + ?, updated_at = ?
3778
3919
  WHERE id = ? AND coordinator_revision = ? AND revision = ? AND plan_revision = ?
3779
3920
  AND ledger_revision = ? AND status NOT IN ('done', 'cancelled')`).run(
3780
3921
  stringify(messages), nextStatus, currentActionId, finalResult ? stringify(finalResult) : null,
3781
- now, workItem.id, current.coordinatorRevision, current.revision,
3922
+ workItem.title, workItem.goal, stringify(workItem.acceptanceCriteria), workItem.deliveryTarget,
3923
+ contractPatch ? 1 : 0, now, workItem.id, current.coordinatorRevision, current.revision,
3782
3924
  current.planRevision, current.ledgerRevision,
3783
3925
  );
3784
3926
  if (Number(changed.changes) !== 1) throw new Error('Dynamic Coordinator completion lost its turn fence');
@@ -4739,7 +4881,7 @@ export class WorkItemStore {
4739
4881
  const ledgerIncrement = workItem.executionSchemaVersion >= 2
4740
4882
  && ['completed', 'failed', 'waiting'].includes(result.outcome) ? 1 : 0;
4741
4883
  this.db.prepare(`UPDATE runs SET status = ?, ended_at = ?, response = ?, summary = ?, evidence = ?,
4742
- acceptance_checks = ?, waiting_reason = ?, error = ?, failure_kind = ?, failure_code = ?, review_decision = ?, contract_patch = ?, checkpoint = ?,
4884
+ outputs = ?, acceptance_checks = ?, waiting_reason = ?, error = ?, failure_kind = ?, failure_code = ?, review_decision = ?, contract_patch = ?, checkpoint = ?,
4743
4885
  loop_count = ?, tool_count = ?, llm_request_count = ?, input_tokens = ?, output_tokens = ?,
4744
4886
  cache_read_tokens = ?, cache_write_tokens = ?, total_tokens = ?,
4745
4887
  progress_revision = progress_revision + 1 WHERE id = ?`).run(
@@ -4748,6 +4890,7 @@ export class WorkItemStore {
4748
4890
  normalizeRunResponse(result.response),
4749
4891
  result.summary || '',
4750
4892
  stringify(normalizeEvidence(result.evidence)),
4893
+ stringify(normalizeOutputs(result.outputs)),
4751
4894
  stringify(Array.isArray(result.acceptanceChecks) ? result.acceptanceChecks : []),
4752
4895
  result.waitingReason || null,
4753
4896
  result.error || null,
@@ -5098,7 +5241,7 @@ export class WorkItemStore {
5098
5241
  ...trigger,
5099
5242
  ledgerRevision: detail.ledgerRevision,
5100
5243
  actionIds: (detail.actions || [])
5101
- .filter(action => !['completed', 'superseded', 'cancelled'].includes(action.status))
5244
+ .filter(action => !['completed', 'closed', 'superseded', 'cancelled'].includes(action.status))
5102
5245
  .map(action => action.id),
5103
5246
  },
5104
5247
  }, `dynamic:reconcile:${workItemId}:${detail.ledgerRevision}`);
@@ -15,6 +15,7 @@ export const BUILT_IN_ACTION_TYPES = Object.freeze([
15
15
  'operate',
16
16
  'deliver',
17
17
  'write',
18
+ 'create_vp',
18
19
  'custom',
19
20
  ]);
20
21
  const STAGE_TYPES = new Set(BUILT_IN_ACTION_TYPES);
@@ -39,6 +40,7 @@ const DEFAULT_STAGE_INSTRUCTIONS = Object.freeze({
39
40
  operate: 'Perform the operational change with explicit preconditions, safety fences, observability, and rollback handling. Verify the live or simulated postcondition from authoritative state, avoid destructive shortcuts, and record the exact evidence needed for handoff.',
40
41
  deliver: 'Deliver only an approved result using the repository release policy. Recheck the reviewed commit and remote state, run required final verification on the immutable delivery tree, publish only the requested artifacts, and report commit, tag, deployment, and residual-risk evidence.',
41
42
  write: 'Produce the requested written deliverable for its intended audience. Use the available evidence, preserve required terminology and structure, avoid unsupported claims, and verify the result against every acceptance criterion.',
43
+ create_vp: 'Create one persistent specialist VP only when no existing VP can execute a required capability. Author a narrow role, traits, and persona with the CreateWorkItemVp tool, then report the new VP id as evidence. Do not clone an existing VP or create a generic replacement.',
42
44
  custom: 'Complete the Action objective using repository facts and the WorkItem contract. State the approach, handle relevant risks and boundary conditions, produce the requested artifact or change, verify the result, and return concrete evidence plus any residual uncertainty.',
43
45
  });
44
46
 
@@ -56,6 +58,7 @@ const DEFAULT_ACTION_BRIEFS = Object.freeze({
56
58
  operate: ['Perform the requested operational change safely.', 'Check preconditions, apply safety fences, preserve rollback options, and verify authoritative state.', 'A verified operational postcondition with handoff and rollback evidence.'],
57
59
  deliver: ['Deliver the approved Work Item result using repository release policy.', 'Recheck immutable reviewed state, run final gates, and publish only the requested artifacts.', 'A traceable delivery with commit, artifact, deployment, and residual-risk evidence.'],
58
60
  write: ['Produce the written deliverable requested by this Action.', 'Use available evidence, the intended audience, and the required terminology and structure.', 'A complete written artifact verified against the acceptance criteria.'],
61
+ create_vp: ['Create the missing specialist VP required by this Work Item.', 'Have the assigned existing VP author a narrow persistent role and persona with the dedicated VP creation tool.', 'A new Agent-local VP whose identity and capability can be selected by later Actions.'],
59
62
  custom: ['Complete the domain-specific objective defined for this Action.', 'Use repository facts, handle relevant risks and boundaries, and verify the produced result.', 'The requested artifact or change with concrete evidence and residual uncertainty.'],
60
63
  });
61
64
 
@@ -521,6 +524,9 @@ export function applyGeneratedPlan(workItem, rawPlan, options = {}) {
521
524
  const input = rawAction && typeof rawAction === 'object' && !Array.isArray(rawAction) ? rawAction : {};
522
525
  const type = generatedActionType(input.type);
523
526
  if (type === 'triage') throw new Error('AI-planned Actions cannot add another triage Action');
527
+ if (type === 'create_vp') {
528
+ throw new Error('create_vp Actions can only be created by the dynamic WorkItem Coordinator');
529
+ }
524
530
  const id = canonicalActionId(input.id, `${type}-${index + 1}`);
525
531
  if (seen.has(id)) throw new Error(`Duplicate AI-planned Action id: ${id}`);
526
532
  if (reservedStageIds.has(id)) {