jg-agent-hq 0.2.0 → 0.4.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 (55) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +7 -4
  3. package/api/README.md +35 -17
  4. package/dist/actions/live/live.d.ts +4 -1
  5. package/dist/actions/live/live.js +22 -6
  6. package/dist/actions/reads.d.ts +1 -1
  7. package/dist/actions/reads.js +4 -2
  8. package/dist/contracts/actions/types.d.ts +6 -6
  9. package/dist/contracts/actions/types.js +1 -1
  10. package/dist/contracts/browser/index.d.ts +4 -0
  11. package/dist/contracts/browser/index.js +6 -0
  12. package/dist/contracts/browser/types.d.ts +2475 -0
  13. package/dist/contracts/browser/types.js +109 -0
  14. package/dist/contracts/handoffs/types.d.ts +5 -5
  15. package/dist/contracts/index.d.ts +4 -0
  16. package/dist/contracts/index.js +6 -0
  17. package/dist/contracts/plans/types.d.ts +25 -25
  18. package/dist/contracts/plans/types.js +9 -10
  19. package/dist/contracts/types.d.ts +96 -30
  20. package/dist/contracts/types.js +10 -2
  21. package/dist/handoffs/types.d.ts +6 -6
  22. package/dist/plans/admission.d.ts +2 -2
  23. package/dist/plans/admission.js +24 -16
  24. package/dist/plans/authority.d.ts +6 -0
  25. package/dist/plans/authority.js +39 -8
  26. package/dist/plans/finalization.d.ts +1 -1
  27. package/dist/plans/finalization.js +4 -3
  28. package/dist/plans/plans.d.ts +3 -3
  29. package/dist/plans/plans.js +9 -11
  30. package/dist/plans/reconciliation.js +3 -3
  31. package/dist/plans/revisions.d.ts +2 -2
  32. package/dist/plans/revisions.js +21 -3
  33. package/dist/plans/types.d.ts +4 -4
  34. package/dist/providers/claude/control/control.d.ts +1 -1
  35. package/dist/providers/claude/history/types.d.ts +39 -11
  36. package/dist/runtime/browser/browser.d.ts +8 -0
  37. package/dist/runtime/browser/browser.js +55 -0
  38. package/dist/runtime/browser/index.d.ts +1 -0
  39. package/dist/runtime/browser/index.js +2 -0
  40. package/dist/runtime/runtime.js +12 -1
  41. package/dist/runtime/types.d.ts +19 -1
  42. package/dist/store/browser/browser.model.d.ts +24 -0
  43. package/dist/store/browser/browser.model.js +75 -0
  44. package/dist/store/browser/index.d.ts +1 -0
  45. package/dist/store/browser/index.js +2 -0
  46. package/dist/store/control/types.d.ts +6 -6
  47. package/dist/store/plans/plans.model.d.ts +2 -2
  48. package/dist/store/plans/plans.model.js +1 -1
  49. package/dist/store/schema.sql +15 -2
  50. package/dist/store/store.js +4 -1
  51. package/dist/store/store.worker.js +11 -0
  52. package/dist/store/types.d.ts +1545 -510
  53. package/dist/store/types.js +17 -1
  54. package/errors/README.md +11 -1
  55. package/package.json +1 -1
@@ -12,9 +12,7 @@ export const describePlan = (artifacts, pendingActionId = null) => {
12
12
  const isPlanApproved = !!coordination.planApproval &&
13
13
  !coordination.planApproval.revokedBy &&
14
14
  coordination.planApproval.revision.planDigest === revision.planDigest;
15
- const isFinalized = !!coordination.finalized &&
16
- revision.breakdownDigest !== null &&
17
- isSamePlanRevision(coordination.finalized.revision, revision);
15
+ const isFinalized = !!coordination.finalized && isSamePlanRevision(coordination.finalized.revision, revision);
18
16
  const isSequenceApproved = isPlanApproved &&
19
17
  isFinalized &&
20
18
  !!coordination.sequenceApproval &&
@@ -22,6 +20,12 @@ export const describePlan = (artifacts, pendingActionId = null) => {
22
20
  isSamePlanRevision(coordination.sequenceApproval.revision, revision);
23
21
  const verified = createCompletionLookup(coordination.completions, coordination.correspondences);
24
22
  const workflow = coordination.revision;
23
+ const invalidated = new Set(coordination.correspondences.flatMap((record) => record.invalidatedReceiptIds));
24
+ const isWholePlanComplete = artifacts.milestones === null &&
25
+ coordination.completions.some((receipt) => receipt.milestoneId === null &&
26
+ receipt.contractDigest === revision.planDigest &&
27
+ receipt.revision.breakdownDigest === null &&
28
+ !invalidated.has(receipt.id));
25
29
  const state = coordination.observedConflict ||
26
30
  pendingActionId ||
27
31
  workflow?.phase === 'blocked' ||
@@ -38,10 +42,11 @@ export const describePlan = (artifacts, pendingActionId = null) => {
38
42
  ? 'plan-approval-required'
39
43
  : !isFinalized
40
44
  ? 'breakdown-required'
41
- : !isSequenceApproved
45
+ : artifacts.milestones !== null && !isSequenceApproved
42
46
  ? 'sequence-approval-required'
43
- : artifacts.parsed.length > 0 &&
44
- artifacts.parsed.every((milestone) => milestone.status === 'done' && verified(milestone))
47
+ : isWholePlanComplete ||
48
+ (artifacts.parsed.length > 0 &&
49
+ artifacts.parsed.every((milestone) => milestone.status === 'done' && verified(milestone)))
45
50
  ? 'completed'
46
51
  : artifacts.parsed.some((milestone) => milestone.status === 'in progress')
47
52
  ? 'in-progress'
@@ -89,6 +94,26 @@ export const describePlan = (artifacts, pendingActionId = null) => {
89
94
  },
90
95
  };
91
96
  };
97
+ /**
98
+ * Checks the current approved unfinished unit, with null identifying an undivided plan.
99
+ * Runtime epoch, action identity and mode remain the caller's separate admission checks.
100
+ * @returns Whether the exact unit is selected by current execution authority.
101
+ */
102
+ export const canExecutePlanUnit = (artifacts, milestoneId) => {
103
+ const status = describePlan(artifacts);
104
+ const execution = artifacts.coordination.execution;
105
+ if (!execution ||
106
+ !status.isPlanApproved ||
107
+ !status.isFinalized ||
108
+ ['blocked', 'completed', 'revising'].includes(status.state))
109
+ return false;
110
+ return artifacts.milestones === null
111
+ ? milestoneId === null && execution.milestoneIds.length === 0
112
+ : status.isSequenceApproved &&
113
+ milestoneId !== null &&
114
+ execution.milestoneIds.includes(milestoneId) &&
115
+ artifacts.parsed.some((milestone) => milestone.id === milestoneId && milestone.status !== 'done');
116
+ };
92
117
  /** Revocation records operator provenance without granting any replacement authority. */
93
118
  const revokeExecution = (coordination, action) => {
94
119
  if (coordination.execution)
@@ -104,7 +129,8 @@ export const updatePlanAuthority = (artifacts, command, runtimeEpoch) => {
104
129
  const coordination = structuredClone(artifacts.coordination);
105
130
  if (command.kind === 'approve') {
106
131
  const status = describePlan(artifacts);
107
- if (command.scope === 'sequence' && (!status.isFinalized || !status.isPlanApproved))
132
+ if (command.scope === 'sequence' &&
133
+ (artifacts.milestones === null || !status.isFinalized || !status.isPlanApproved))
108
134
  conflict();
109
135
  const presentation = command.scope === 'plan' ? artifacts.hashes.plan : artifacts.hashes.milestones;
110
136
  if (presentation !== command.presentationHash)
@@ -139,7 +165,12 @@ export const updatePlanAuthority = (artifacts, command, runtimeEpoch) => {
139
165
  coordination.revision.phase !== 'finished')
140
166
  conflict();
141
167
  if (status.state === 'blocked' ||
142
- !status.isSequenceApproved ||
168
+ status.state === 'completed' ||
169
+ !status.isPlanApproved ||
170
+ !status.isFinalized ||
171
+ (artifacts.milestones === null
172
+ ? command.milestoneIds.length !== 0
173
+ : !status.isSequenceApproved || command.milestoneIds.length === 0) ||
143
174
  command.milestoneIds.some((id) => !artifacts.parsed.some((milestone) => milestone.id === id && milestone.status !== 'done')))
144
175
  conflict();
145
176
  if (coordination.revision)
@@ -8,6 +8,6 @@ import type { IPlanArtifacts } from './types.js';
8
8
  * - -1: The HQ plan is invalid
9
9
  */
10
10
  export declare const finalizeInitialSequence: (artifacts: IPlanArtifacts) => {
11
- content: string;
11
+ content: string | null;
12
12
  revision: IPlanRevision;
13
13
  };
@@ -14,10 +14,11 @@ const conflict = () => {
14
14
  * - -1: The HQ plan is invalid
15
15
  */
16
16
  export const finalizeInitialSequence = (artifacts) => {
17
- if (artifacts.milestones === null ||
18
- (artifacts.coordination.finalized &&
19
- !isSamePlanRevision(artifacts.coordination.finalized.revision, artifacts.revision)))
17
+ if (artifacts.coordination.finalized &&
18
+ !isSamePlanRevision(artifacts.coordination.finalized.revision, artifacts.revision))
20
19
  conflict();
20
+ if (artifacts.milestones === null)
21
+ return { content: null, revision: artifacts.revision };
21
22
  let milestones = artifacts.milestones.replace(/\r\n?/g, '\n');
22
23
  // insertion offsets are applied backwards so original positions stay valid
23
24
  for (const milestone of [...artifacts.parsed].reverse())
@@ -116,7 +116,7 @@ export declare const createPlans: (coordinationRoot: string, roots: string[], st
116
116
  * - -1: HQ lock ownership changed before release
117
117
  */
118
118
  checkpoint(target: IPlanCommand["target"], revisionId: string, phaseActionId: string): Promise<void>;
119
- admitNative(action: import("../contracts/index.js").IActionEnvelope, prepared: import("../store/control/types.js").IAdmission["prepared"]): Promise<{
119
+ admitNative(action: import("../contracts/index.js").IActionEnvelope, prepared: import("../store/control/types.ts").IAdmission["prepared"]): Promise<{
120
120
  receipt: {
121
121
  id: string;
122
122
  digest: string;
@@ -134,7 +134,7 @@ export declare const createPlans: (coordinationRoot: string, roots: string[], st
134
134
  scope: {
135
135
  kind: "execution";
136
136
  authorizationId: string;
137
- milestoneId: string;
137
+ milestoneId: string | null;
138
138
  mode: "supervised" | "auto";
139
139
  revision: {
140
140
  planDigest: string;
@@ -192,7 +192,7 @@ export declare const createPlans: (coordinationRoot: string, roots: string[], st
192
192
  scope: {
193
193
  kind: "execution";
194
194
  authorizationId: string;
195
- milestoneId: string;
195
+ milestoneId: string | null;
196
196
  mode: "supervised" | "auto";
197
197
  revision: {
198
198
  planDigest: string;
@@ -4,7 +4,7 @@ import { PlanMilestoneSchema, } from '../contracts/index.js';
4
4
  import { getErrorKind } from '../errors/index.js';
5
5
  import { withHqLock } from '../locks/index.js';
6
6
  import { resolveWorkspaceTarget } from '../runtime/state/index.js';
7
- import { describePlan, updatePlanAuthority } from './authority.js';
7
+ import { canExecutePlanUnit, describePlan, updatePlanAuthority } from './authority.js';
8
8
  import { createPlanAdmission } from './admission.js';
9
9
  import { readPlanPage } from './projections.js';
10
10
  import { finalizeInitialSequence } from './finalization.js';
@@ -340,16 +340,14 @@ export const createPlans = (coordinationRoot, roots, store, runtimeEpoch, parseC
340
340
  const execution = artifacts.coordination.execution;
341
341
  if (pendingActionId ||
342
342
  status.state === 'blocked' ||
343
- !status.isSequenceApproved ||
344
343
  !execution ||
345
344
  execution.runtimeEpoch !== runtimeEpoch ||
346
345
  execution.revokedBy ||
347
346
  execution.action.id !== command.authorizationId ||
348
347
  execution.mode !== command.mode ||
349
- !execution.milestoneIds.includes(command.milestoneId) ||
348
+ !canExecutePlanUnit(artifacts, command.milestoneId) ||
350
349
  !isSamePlanRevision(command.expected, artifacts.revision) ||
351
- !isSamePlanRevision(execution.revision, artifacts.revision) ||
352
- !artifacts.parsed.some((milestone) => milestone.id === command.milestoneId && milestone.status !== 'done'))
350
+ !isSamePlanRevision(execution.revision, artifacts.revision))
353
351
  conflict();
354
352
  return {
355
353
  kind: 'execution-check',
@@ -367,9 +365,7 @@ export const createPlans = (coordinationRoot, roots, store, runtimeEpoch, parseC
367
365
  await apply(previous);
368
366
  artifacts = await readPlanArtifacts(command.target, roots, parseContract);
369
367
  const current = describeCurrent(artifacts, await store.getPendingPlanOperationId(artifacts.directory));
370
- return command.kind === 'finalize' &&
371
- current.isFinalized &&
372
- artifacts.milestones !== null
368
+ return command.kind === 'finalize' && current.isFinalized
373
369
  ? { kind: 'finalized', result: current, content: artifacts.milestones }
374
370
  : { kind: 'status', result: current };
375
371
  }
@@ -449,7 +445,7 @@ export const createPlans = (coordinationRoot, roots, store, runtimeEpoch, parseC
449
445
  conflict();
450
446
  if ((await store.getPlanThreads(artifacts.directory)).length)
451
447
  conflict();
452
- if (!workflow.checkpoint?.isQuiescent || artifacts.milestones === null)
448
+ if (!workflow.checkpoint?.isQuiescent)
453
449
  conflict();
454
450
  const proposal = workflow.proposal ?? proposeReconciliation(artifacts, command.action);
455
451
  if (proposal.inspectionHash !== artifacts.hashes.milestones ||
@@ -475,7 +471,9 @@ export const createPlans = (coordinationRoot, roots, store, runtimeEpoch, parseC
475
471
  }
476
472
  else if (command.confirmation)
477
473
  conflict();
478
- const content = renderReconciledSequence(artifacts.milestones, drafts);
474
+ const content = artifacts.milestones === null
475
+ ? null
476
+ : renderReconciledSequence(artifacts.milestones, drafts);
479
477
  const revision = computePlanRevision({
480
478
  ...artifacts.contract,
481
479
  milestones: content,
@@ -498,7 +496,7 @@ export const createPlans = (coordinationRoot, roots, store, runtimeEpoch, parseC
498
496
  coordination.finalized = {
499
497
  action: command.action,
500
498
  revision: finalized.revision,
501
- fileHash: hashPlanContent(milestones),
499
+ fileHash: milestones === null ? null : hashPlanContent(milestones),
502
500
  createdAt: Date.now(),
503
501
  };
504
502
  coordination.baseline = refreshPlanBaseline(artifacts, milestones, finalized.revision);
@@ -22,7 +22,7 @@ export const createCompletionLookup = (completions, records) => {
22
22
  });
23
23
  const direct = new Set(completions
24
24
  .filter((receipt) => !invalidated.has(receipt.id) &&
25
- receipt.reopenActionId === (reopened.get(receipt.milestoneId)?.id ?? null))
25
+ receipt.reopenActionId === (reopened.get(receipt.milestoneId ?? '')?.id ?? null))
26
26
  .map((receipt) => JSON.stringify([receipt.milestoneId, receipt.contractDigest])));
27
27
  const links = new Map();
28
28
  for (const [recordIndex, record] of records.entries()) {
@@ -67,7 +67,7 @@ export const createCompletionLookup = (completions, records) => {
67
67
  */
68
68
  export const proposeReconciliation = (artifacts, action) => {
69
69
  const checkpoint = artifacts.coordination.revision?.checkpoint;
70
- if (!checkpoint || artifacts.milestones === null)
70
+ if (!checkpoint)
71
71
  conflict();
72
72
  const sources = checkpoint.entries;
73
73
  const oldByContract = new Map();
@@ -186,7 +186,7 @@ export const confirmReconciliation = (artifacts, proposal, action, decisions) =>
186
186
  }
187
187
  const invalidatedReceiptIds = decision.relation === 'reopened'
188
188
  ? artifacts.coordination.completions
189
- .filter((receipt) => decision.sourceIds.includes(receipt.milestoneId))
189
+ .filter((receipt) => receipt.milestoneId !== null && decision.sourceIds.includes(receipt.milestoneId))
190
190
  .map((receipt) => receipt.id)
191
191
  : [];
192
192
  records.push({
@@ -37,7 +37,7 @@ export declare const finishPlanCheckpoint: (artifacts: IPlanArtifacts, revisionI
37
37
  */
38
38
  export declare const recordPlanCompletion: (artifacts: IPlanArtifacts, input: {
39
39
  action: IPlanOperatorAction;
40
- milestoneId: string;
40
+ milestoneId: string | null;
41
41
  checks: IPlanCompletion["checks"];
42
42
  }, checkouts: IPlanWorktreeProof[]) => IPlanCompletion;
43
43
  /**
@@ -46,6 +46,6 @@ export declare const recordPlanCompletion: (artifacts: IPlanArtifacts, input: {
46
46
  * @throws
47
47
  * - -1: The HQ plan is invalid
48
48
  */
49
- export declare const refreshPlanBaseline: (artifacts: IPlanArtifacts, content: string, revision: IPlanCheckpoint["revision"]) => IPlanCheckpoint;
49
+ export declare const refreshPlanBaseline: (artifacts: IPlanArtifacts, content: string | null, revision: IPlanCheckpoint["revision"]) => IPlanCheckpoint;
50
50
  /** Builds indexed retained evidence without exposing whole coordination collections to clients. */
51
51
  export declare const projectPlanRevision: (artifacts: IPlanArtifacts) => IPlanRevisionProjection;
@@ -24,8 +24,6 @@ export const capturePlanCheckpoint = (artifacts, threadIds) => {
24
24
  isQuiescent: false,
25
25
  capturedAt: Date.now(),
26
26
  };
27
- if (artifacts.milestones === null || !artifacts.revision.breakdownDigest)
28
- conflict();
29
27
  return {
30
28
  revision: artifacts.revision,
31
29
  plan: artifacts.plan,
@@ -120,6 +118,24 @@ export const finishPlanCheckpoint = (artifacts, revisionId, checkouts) => {
120
118
  * - -1: The HQ action conflicts with its recorded state
121
119
  */
122
120
  export const recordPlanCompletion = (artifacts, input, checkouts) => {
121
+ if (input.milestoneId === null) {
122
+ if (artifacts.milestones !== null)
123
+ conflict();
124
+ return {
125
+ id: input.action.id,
126
+ action: input.action,
127
+ milestoneId: null,
128
+ revision: artifacts.revision,
129
+ contractDigest: artifacts.revision.planDigest,
130
+ title: 'Whole plan',
131
+ contract: artifacts.contract.body,
132
+ sharedDigest: null,
133
+ checkouts,
134
+ checks: input.checks,
135
+ completedAt: Date.now(),
136
+ reopenActionId: null,
137
+ };
138
+ }
123
139
  const milestone = artifacts.parsed.find((entry) => entry.id === input.milestoneId) ??
124
140
  artifacts.coordination.revision?.checkpoint?.entries.find((entry) => entry.id === input.milestoneId);
125
141
  if (!milestone?.id || !milestone.contractDigest)
@@ -155,7 +171,9 @@ export const refreshPlanBaseline = (artifacts, content, revision) => {
155
171
  revision,
156
172
  plan: artifacts.plan,
157
173
  milestones: content,
158
- entries: parsePlanMilestones(content).map((entry) => PlanSnapshotMilestoneSchema.parse(entry)),
174
+ entries: content === null
175
+ ? []
176
+ : parsePlanMilestones(content).map((entry) => PlanSnapshotMilestoneSchema.parse(entry)),
159
177
  checkouts: previous?.checkouts ?? [],
160
178
  threadIds: previous?.threadIds ?? [],
161
179
  isQuiescent: previous?.isQuiescent ?? true,
@@ -156,7 +156,7 @@ export declare const PlanRevisionProjectionSchema: z.ZodObject<{
156
156
  reference: z.ZodString;
157
157
  }, z.core.$strict>;
158
158
  }, z.core.$strict>;
159
- milestoneId: z.ZodUUID;
159
+ milestoneId: z.ZodNullable<z.ZodUUID>;
160
160
  revision: z.ZodObject<{
161
161
  planDigest: z.ZodString;
162
162
  breakdownDigest: z.ZodNullable<z.ZodString>;
@@ -164,7 +164,7 @@ export declare const PlanRevisionProjectionSchema: z.ZodObject<{
164
164
  contractDigest: z.ZodString;
165
165
  title: z.ZodString;
166
166
  contract: z.ZodString;
167
- sharedDigest: z.ZodString;
167
+ sharedDigest: z.ZodNullable<z.ZodString>;
168
168
  checkouts: z.ZodArray<z.ZodObject<{
169
169
  repository: z.ZodString;
170
170
  directory: z.ZodString;
@@ -290,7 +290,7 @@ export declare const PlanRevisionPageResultSchema: z.ZodObject<{
290
290
  reference: z.ZodString;
291
291
  }, z.core.$strict>;
292
292
  }, z.core.$strict>;
293
- milestoneId: z.ZodUUID;
293
+ milestoneId: z.ZodNullable<z.ZodUUID>;
294
294
  revision: z.ZodObject<{
295
295
  planDigest: z.ZodString;
296
296
  breakdownDigest: z.ZodNullable<z.ZodString>;
@@ -298,7 +298,7 @@ export declare const PlanRevisionPageResultSchema: z.ZodObject<{
298
298
  contractDigest: z.ZodString;
299
299
  title: z.ZodString;
300
300
  contract: z.ZodString;
301
- sharedDigest: z.ZodString;
301
+ sharedDigest: z.ZodNullable<z.ZodString>;
302
302
  checkouts: z.ZodArray<z.ZodObject<{
303
303
  repository: z.ZodString;
304
304
  directory: z.ZodString;
@@ -47,7 +47,7 @@ export declare const createClaudeControl: (additionalDirectories?: string[]) =>
47
47
  * @throws
48
48
  * - -1: The Claude integration failed
49
49
  */
50
- readSkills(nativeId: string): Promise<import("../../types.js").IProviderSkills>;
50
+ readSkills(nativeId: string): Promise<import("../../types.ts").IProviderSkills>;
51
51
  /**
52
52
  * Names only the native session selected by the admitted action.
53
53
  * @returns Native acknowledgement of the name change.
@@ -1,5 +1,31 @@
1
1
  import { z } from 'zod';
2
2
  export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscriminatedUnion<[z.ZodObject<{
3
+ kind: z.ZodLiteral<"thread-plan">;
4
+ result: z.ZodNullable<z.ZodObject<{
5
+ directory: z.ZodString;
6
+ scope: z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
7
+ kind: z.ZodLiteral<"execution">;
8
+ authorizationId: z.ZodUUID;
9
+ milestoneId: z.ZodNullable<z.ZodUUID>;
10
+ mode: z.ZodEnum<{
11
+ supervised: "supervised";
12
+ auto: "auto";
13
+ }>;
14
+ revision: z.ZodObject<{
15
+ planDigest: z.ZodString;
16
+ breakdownDigest: z.ZodNullable<z.ZodString>;
17
+ }, z.core.$strict>;
18
+ }, z.core.$strict>, z.ZodObject<{
19
+ kind: z.ZodLiteral<"planning">;
20
+ revisionId: z.ZodUUID;
21
+ invocationId: z.ZodUUID;
22
+ }, z.core.$strict>], "kind">>;
23
+ observedRevision: z.ZodObject<{
24
+ planDigest: z.ZodString;
25
+ breakdownDigest: z.ZodNullable<z.ZodString>;
26
+ }, z.core.$strict>;
27
+ }, z.core.$strict>>;
28
+ }, z.core.$strip>, z.ZodObject<{
3
29
  kind: z.ZodLiteral<"status">;
4
30
  result: z.ZodObject<{
5
31
  runtimeEpoch: z.ZodUUID;
@@ -110,7 +136,7 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
110
136
  planDigest: z.ZodString;
111
137
  breakdownDigest: z.ZodNullable<z.ZodString>;
112
138
  }, z.core.$strict>;
113
- fileHash: z.ZodString;
139
+ fileHash: z.ZodNullable<z.ZodString>;
114
140
  createdAt: z.ZodNumber;
115
141
  }, z.core.$strict>>;
116
142
  planApproval: z.ZodNullable<z.ZodObject<{
@@ -330,7 +356,7 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
330
356
  planDigest: z.ZodString;
331
357
  breakdownDigest: z.ZodNullable<z.ZodString>;
332
358
  }, z.core.$strict>;
333
- fileHash: z.ZodString;
359
+ fileHash: z.ZodNullable<z.ZodString>;
334
360
  createdAt: z.ZodNumber;
335
361
  }, z.core.$strict>>;
336
362
  planApproval: z.ZodNullable<z.ZodObject<{
@@ -518,7 +544,7 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
518
544
  reference: z.ZodString;
519
545
  }, z.core.$strict>;
520
546
  }, z.core.$strict>;
521
- milestoneId: z.ZodUUID;
547
+ milestoneId: z.ZodNullable<z.ZodUUID>;
522
548
  revision: z.ZodObject<{
523
549
  planDigest: z.ZodString;
524
550
  breakdownDigest: z.ZodNullable<z.ZodString>;
@@ -526,7 +552,7 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
526
552
  contractDigest: z.ZodString;
527
553
  title: z.ZodString;
528
554
  contract: z.ZodString;
529
- sharedDigest: z.ZodString;
555
+ sharedDigest: z.ZodNullable<z.ZodString>;
530
556
  checkouts: z.ZodArray<z.ZodObject<{
531
557
  repository: z.ZodString;
532
558
  directory: z.ZodString;
@@ -674,7 +700,7 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
674
700
  planDigest: z.ZodString;
675
701
  breakdownDigest: z.ZodNullable<z.ZodString>;
676
702
  }, z.core.$strict>;
677
- fileHash: z.ZodString;
703
+ fileHash: z.ZodNullable<z.ZodString>;
678
704
  createdAt: z.ZodNumber;
679
705
  }, z.core.$strict>>;
680
706
  planApproval: z.ZodNullable<z.ZodObject<{
@@ -813,7 +839,7 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
813
839
  }, z.core.$strict>>;
814
840
  }, z.core.$strict>;
815
841
  }, z.core.$strip>;
816
- content: z.ZodString;
842
+ content: z.ZodNullable<z.ZodString>;
817
843
  }, z.core.$strip>, z.ZodObject<{
818
844
  kind: z.ZodLiteral<"milestones">;
819
845
  result: z.ZodObject<{
@@ -894,7 +920,7 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
894
920
  scope: z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
895
921
  kind: z.ZodLiteral<"execution">;
896
922
  authorizationId: z.ZodUUID;
897
- milestoneId: z.ZodUUID;
923
+ milestoneId: z.ZodNullable<z.ZodUUID>;
898
924
  mode: z.ZodEnum<{
899
925
  supervised: "supervised";
900
926
  auto: "auto";
@@ -1027,7 +1053,7 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
1027
1053
  scope: z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
1028
1054
  kind: z.ZodLiteral<"execution">;
1029
1055
  authorizationId: z.ZodUUID;
1030
- milestoneId: z.ZodUUID;
1056
+ milestoneId: z.ZodNullable<z.ZodUUID>;
1031
1057
  mode: z.ZodEnum<{
1032
1058
  supervised: "supervised";
1033
1059
  auto: "auto";
@@ -1364,7 +1390,7 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
1364
1390
  scope: z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
1365
1391
  kind: z.ZodLiteral<"execution">;
1366
1392
  authorizationId: z.ZodUUID;
1367
- milestoneId: z.ZodUUID;
1393
+ milestoneId: z.ZodNullable<z.ZodUUID>;
1368
1394
  mode: z.ZodEnum<{
1369
1395
  supervised: "supervised";
1370
1396
  auto: "auto";
@@ -1440,7 +1466,7 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
1440
1466
  scope: z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
1441
1467
  kind: z.ZodLiteral<"execution">;
1442
1468
  authorizationId: z.ZodUUID;
1443
- milestoneId: z.ZodUUID;
1469
+ milestoneId: z.ZodNullable<z.ZodUUID>;
1444
1470
  mode: z.ZodEnum<{
1445
1471
  supervised: "supervised";
1446
1472
  auto: "auto";
@@ -1628,7 +1654,7 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
1628
1654
  scope: z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
1629
1655
  kind: z.ZodLiteral<"execution">;
1630
1656
  authorizationId: z.ZodUUID;
1631
- milestoneId: z.ZodUUID;
1657
+ milestoneId: z.ZodNullable<z.ZodUUID>;
1632
1658
  mode: z.ZodEnum<{
1633
1659
  supervised: "supervised";
1634
1660
  auto: "auto";
@@ -1719,6 +1745,8 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
1719
1745
  }, z.core.$strip>, z.ZodObject<{
1720
1746
  kind: z.ZodLiteral<"live-items">;
1721
1747
  result: z.ZodObject<{
1748
+ revision: z.ZodNumber;
1749
+ unchanged: z.ZodBoolean;
1722
1750
  data: z.ZodArray<z.ZodObject<{
1723
1751
  nativeId: z.ZodString;
1724
1752
  turnId: z.ZodString;
@@ -0,0 +1,8 @@
1
+ import { type IBrowserAction, type IBrowserReceipt } from '../../contracts/index.js';
2
+ import type { IStore } from '../../store/index.js';
3
+ /** Serializes host admission and owns effects independently of an HTTP connection. */
4
+ export declare const createBrowserActions: (store: IStore) => {
5
+ read: (id: string, operatorId: string) => Promise<IBrowserReceipt | null>;
6
+ perform: (input: IBrowserAction, operatorId: string, execute: (action: IBrowserAction, nativeActionId: string) => Promise<void>) => Promise<IBrowserReceipt>;
7
+ close: () => Promise<void>;
8
+ };
@@ -0,0 +1,55 @@
1
+ import { Exception } from 'error-message-utils';
2
+ import { z } from 'zod';
3
+ import { BrowserActionSchema, } from '../../contracts/index.js';
4
+ const OperatorSchema = z.string().min(1).max(256);
5
+ /** Serializes host admission and owns effects independently of an HTTP connection. */
6
+ export const createBrowserActions = (store) => {
7
+ const pending = new Set();
8
+ let isClosing = false;
9
+ const read = async (id, operatorId) => {
10
+ const parsed = z
11
+ .object({ id: z.uuid(), operatorId: OperatorSchema })
12
+ .safeParse({ id, operatorId });
13
+ if (!parsed.success)
14
+ throw new Exception('The HQ request is invalid', undefined, { kind: 'invalid-request' });
15
+ const receipt = await store.getBrowserAction(id, operatorId);
16
+ if (receipt?.kind === 'native')
17
+ receipt.native = await store.getAction(receipt.nativeActionId);
18
+ return receipt;
19
+ };
20
+ const perform = (input, operatorId, execute) => {
21
+ if (isClosing || pending.size >= 32)
22
+ return Promise.reject(new Exception('The HQ action conflicts with its recorded state', undefined, {
23
+ kind: 'state-conflict',
24
+ }));
25
+ const task = (async () => {
26
+ const action = BrowserActionSchema.safeParse(input);
27
+ if (!action.success || !OperatorSchema.safeParse(operatorId).success)
28
+ throw new Exception('The HQ request is invalid', undefined, { kind: 'invalid-request' });
29
+ const admission = await store.admitBrowserAction(action.data, operatorId);
30
+ if (admission.isNew) {
31
+ try {
32
+ await execute(action.data, admission.receipt.nativeActionId);
33
+ await store.settleBrowserAction(action.data.id, operatorId, 'acknowledged');
34
+ }
35
+ catch (cause) {
36
+ // effects may have committed before an error; only reconciliation can establish their outcome
37
+ await store.settleBrowserAction(action.data.id, operatorId, 'uncertain');
38
+ throw cause;
39
+ }
40
+ }
41
+ return (await read(action.data.id, operatorId));
42
+ })();
43
+ pending.add(task);
44
+ void task.finally(() => pending.delete(task)).catch(() => undefined);
45
+ return task;
46
+ };
47
+ return {
48
+ read,
49
+ perform,
50
+ close: async () => {
51
+ isClosing = true;
52
+ await Promise.allSettled([...pending]);
53
+ },
54
+ };
55
+ };
@@ -0,0 +1 @@
1
+ export { createBrowserActions } from './browser.js';
@@ -0,0 +1,2 @@
1
+ // host-action lifecycle
2
+ export { createBrowserActions } from './browser.js';
@@ -12,6 +12,7 @@ import { createSkills } from '../skills/index.js';
12
12
  import { createStore } from '../store/index.js';
13
13
  import { createPlans } from '../plans/index.js';
14
14
  import { claimRuntimeOwner, prepareDatabase, prepareActionKey, prepareStateRoot, resolveAllowedRoots, resolveWorkspaceTarget, } from './state/index.js';
15
+ import { createBrowserActions } from './browser/index.js';
15
16
  import { AgentHqOptionsSchema } from './types.js';
16
17
  const UnconfirmedCleanupSchema = z.object({ cleanupUnconfirmed: z.literal(true) });
17
18
  /**
@@ -67,6 +68,7 @@ export const createAgentHq = async (options) => {
67
68
  await ownership.release();
68
69
  throw error;
69
70
  }
71
+ const browser = createBrowserActions(store);
70
72
  const native = provider;
71
73
  const projection = store;
72
74
  const claude = createClaudeProvider(projection);
@@ -137,6 +139,7 @@ export const createAgentHq = async (options) => {
137
139
  });
138
140
  const close = () => (closing ??= (async () => {
139
141
  try {
142
+ await browser.close();
140
143
  await Promise.allSettled([...revisionJobs.values()]);
141
144
  const results = await Promise.allSettled([ipc?.close(), actions.close(), claude.close()]);
142
145
  const storeCleanup = await Promise.allSettled([projection.close()]);
@@ -213,6 +216,7 @@ export const createAgentHq = async (options) => {
213
216
  kind: command.kind,
214
217
  result: { name: await projection.suggestThreadName(suggestThreadName(command.topic)) },
215
218
  };
219
+ case 'thread-plan':
216
220
  case 'managed-thread-name':
217
221
  case 'receipt':
218
222
  case 'managed-threads':
@@ -264,5 +268,12 @@ export const createAgentHq = async (options) => {
264
268
  await close();
265
269
  throw error;
266
270
  }
267
- return { stateRoot, closed, request, close };
271
+ return {
272
+ stateRoot,
273
+ closed,
274
+ request,
275
+ close,
276
+ performBrowserAction: browser.perform,
277
+ readBrowserAction: browser.read,
278
+ };
268
279
  };
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { IHqRequestInput, IHqResponse } from '../contracts/index.js';
2
+ import type { IHqRequestInput, IHqResponse, IBrowserAction, IBrowserReceipt } from '../contracts/index.js';
3
3
  import type { IPlanContractParser } from '../plans/index.js';
4
4
  export declare const AgentHqOptionsSchema: z.ZodObject<{
5
5
  stateRoot: z.ZodString;
@@ -11,6 +11,24 @@ export declare const AgentHqOptionsSchema: z.ZodObject<{
11
11
  }, z.core.$strict>;
12
12
  export type IAgentHqOptions = z.infer<typeof AgentHqOptionsSchema>;
13
13
  export type IAgentHq = {
14
+ /**
15
+ * Runs one trusted host dispatcher after durable admission. Never expose the callback as RPC.
16
+ * Dispatcher failures propagate after the receipt becomes uncertain.
17
+ * @returns Its receipt; replays do not execute the dispatcher.
18
+ * @throws
19
+ * - -1: The HQ request is invalid
20
+ * - -1: The HQ action conflicts with its recorded state
21
+ * - -1: The HQ store failed
22
+ */
23
+ performBrowserAction(input: IBrowserAction, operatorId: string, execute: (action: IBrowserAction, nativeActionId: string) => Promise<void>): Promise<IBrowserReceipt>;
24
+ /**
25
+ * Reconciles a selected receipt for its authenticated operator without repeating effects.
26
+ * @returns The receipt, or null for another operator or unknown ID.
27
+ * @throws
28
+ * - -1: The HQ request is invalid
29
+ * - -1: The HQ store failed
30
+ */
31
+ readBrowserAction(id: string, operatorId: string): Promise<IBrowserReceipt | null>;
14
32
  stateRoot: string;
15
33
  closed: Promise<void>;
16
34
  /**