@fro.bot/systematic 3.5.0 → 3.5.1

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.
package/dist/index.js CHANGED
@@ -3320,9 +3320,20 @@ function applyUnitStart(marker, epoch, context) {
3320
3320
  return;
3321
3321
  }
3322
3322
  if (current.unitId === marker.unitId) {
3323
- if (!sameUnitDeclaration(current, marker))
3323
+ if (current.state === "completed")
3324
+ return "out-of-order";
3325
+ if (!sameUnitDeclaration(current, marker) && unitHasMintedEvidence(current, context)) {
3326
+ return "out-of-order";
3327
+ }
3328
+ if (sameUnitDeclaration(current, marker) && current.transitionDigest !== marker.transitionDigest) {
3324
3329
  return "conflicting-marker";
3325
- return current.state === "completed" ? "out-of-order" : "conflicting-marker";
3330
+ }
3331
+ if (!unitDeclarationExtends(current, marker))
3332
+ return "conflicting-marker";
3333
+ if (sameUnitDeclaration(current, marker))
3334
+ return;
3335
+ context.progression = { epoch, unit: snapshot };
3336
+ return;
3326
3337
  }
3327
3338
  if (current.state !== "completed")
3328
3339
  return "out-of-order";
@@ -3346,9 +3357,26 @@ function applyUnitComplete(marker, epoch, context) {
3346
3357
  };
3347
3358
  return;
3348
3359
  }
3360
+ function unitHasMintedEvidence(current, context) {
3361
+ return [...context.mintByReceipt.values()].some((envelope) => envelope.canonical.epochDigest === current.epochDigest && envelope.canonical.unitDigest === current.unitDigest);
3362
+ }
3349
3363
  function sameUnitDeclaration(current, marker) {
3350
3364
  return current.epochDigest === marker.epochDigest && current.unitDigest === marker.unitDigest && current.family === marker.family && JSON.stringify(current.requiredOperations) === JSON.stringify(marker.requiredOperations) && JSON.stringify(current.resourceScopes) === JSON.stringify(marker.resourceScopes);
3351
3365
  }
3366
+ function unitDeclarationExtends(current, marker) {
3367
+ if (current.epochDigest !== marker.epochDigest || current.unitDigest !== marker.unitDigest || current.family !== marker.family) {
3368
+ return false;
3369
+ }
3370
+ const nextOperations = new Set(marker.requiredOperations);
3371
+ if (current.requiredOperations.some((operation) => !nextOperations.has(operation))) {
3372
+ return false;
3373
+ }
3374
+ const nextScopes = new Map(marker.resourceScopes.map((scope) => [
3375
+ scope.operation,
3376
+ scope.resourceIdentity
3377
+ ]));
3378
+ return [...current.resourceScopes].every((scope) => nextScopes.get(scope.operation) === scope.resourceIdentity);
3379
+ }
3352
3380
  function foldProgressionMarker(marker, context) {
3353
3381
  return marker.target === "epoch" ? applyEpochProgression(marker, context) : applyUnitProgression(marker, context);
3354
3382
  }
@@ -8473,7 +8501,11 @@ function cloneUnit(unit) {
8473
8501
  requiredOperations: unit.requiredOperations,
8474
8502
  requiredResourceOperations: Object.freeze([
8475
8503
  ...unit.declaredResourceOperations
8476
- ])
8504
+ ]),
8505
+ resourceScopes: Object.freeze([...unit.resourceScopes].map(([operation, resourceIdentity]) => ({
8506
+ operation,
8507
+ resourceIdentity
8508
+ })))
8477
8509
  });
8478
8510
  }
8479
8511
  function familyForSkill(skill) {
@@ -8900,6 +8932,13 @@ function createWorkflowGuard(options) {
8900
8932
  return "receipt-mismatch";
8901
8933
  return context.worktreeIdentity !== currentWorktreeIdentity ? "receipt-mismatch" : undefined;
8902
8934
  }
8935
+ function currentOperationContext() {
8936
+ return Object.freeze({
8937
+ workspaceIdentity: currentWorkspaceIdentity,
8938
+ ...currentRepositoryIdentity === undefined ? {} : { repositoryIdentity: currentRepositoryIdentity },
8939
+ ...currentWorktreeIdentity === undefined ? {} : { worktreeIdentity: currentWorktreeIdentity }
8940
+ });
8941
+ }
8903
8942
  function resourceBeforeReason(input, unit) {
8904
8943
  if (!operationUsesResource(input.operation))
8905
8944
  return;
@@ -9591,9 +9630,10 @@ function createWorkflowGuard(options) {
9591
9630
  }
9592
9631
  return;
9593
9632
  }
9594
- function mergeResourceScopes(trustedPolicy, modelScopes) {
9595
- const result = new Map(runtimeScopes);
9596
- for (const scopes of [trustedPolicy.resourceScopes, modelScopes]) {
9633
+ function mergeResourceScopes(trustedPolicy, modelScopes, existingScopes) {
9634
+ const result = new Map(existingScopes ?? runtimeScopes);
9635
+ const scopesToMerge = existingScopes ? [runtimeScopes, trustedPolicy.resourceScopes, modelScopes] : [trustedPolicy.resourceScopes, modelScopes];
9636
+ for (const scopes of scopesToMerge) {
9597
9637
  for (const [operation, resource] of scopes) {
9598
9638
  const existing = result.get(operation);
9599
9639
  if (existing && existing !== resource)
@@ -9603,22 +9643,71 @@ function createWorkflowGuard(options) {
9603
9643
  }
9604
9644
  return result;
9605
9645
  }
9606
- function requiredOperationsFor(trustedPolicy, model, resourceScopes) {
9646
+ function requiredOperationsFor(trustedPolicy, model, resourceScopes, existingOperations = []) {
9607
9647
  return Object.freeze([
9608
9648
  ...new Set([
9609
9649
  ...MANDATORY_OPERATIONS,
9610
9650
  ...runtimeRequired,
9651
+ ...existingOperations,
9611
9652
  ...trustedPolicy.expectedOperations,
9612
9653
  ...model.expectedOperations,
9613
9654
  ...resourceScopes.keys()
9614
9655
  ])
9615
9656
  ]);
9616
9657
  }
9658
+ function resourceIdentitiesMatchUnit(unit) {
9659
+ const expected = new Map(runtimeScopes);
9660
+ for (const [operation, resource] of unit.resourceScopes) {
9661
+ expected.set(operation, resource);
9662
+ }
9663
+ if (currentResourceIdentities.size !== expected.size)
9664
+ return false;
9665
+ for (const [operation, resource] of expected) {
9666
+ if (currentResourceIdentities.get(operation) !== resource)
9667
+ return false;
9668
+ }
9669
+ return true;
9670
+ }
9671
+ function pristineActiveUnit(unit) {
9672
+ return unit.evidence.size === 0 && unit.issues.size === 0 && unit.staleReceiptIds.size === 0 && unit.recoveredReceiptIds.size === 0 && unit.operationStates.size === 0 && unit.ledgerContexts.size === 0 && globalIssue === undefined && transitionsByCall.size === 0 && terminalOperationCalls.size === 0 && currentResourceRevisionIdentities.size === 0 && currentPullRequestFingerprint === undefined && currentWorkspaceIdentity === initialWorkspaceIdentity && currentRepositoryIdentity === initialRepositoryIdentity && currentWorktreeIdentity === initialWorktreeIdentity && resourceIdentitiesMatchUnit(unit);
9673
+ }
9674
+ function declarationChanged(unit, requiredOperations, resourceScopes) {
9675
+ if (JSON.stringify(unit.requiredOperations) !== JSON.stringify(requiredOperations)) {
9676
+ return true;
9677
+ }
9678
+ if (unit.resourceScopes.size !== resourceScopes.size)
9679
+ return true;
9680
+ for (const [operation, resource] of resourceScopes) {
9681
+ if (unit.resourceScopes.get(operation) !== resource)
9682
+ return true;
9683
+ }
9684
+ return false;
9685
+ }
9686
+ function startActiveUnit(parsed, trustedPolicy, unit) {
9687
+ if (!pristineActiveUnit(unit)) {
9688
+ return { status: "rejected", reasonCode: "unit-active" };
9689
+ }
9690
+ const resourceScopes = mergeResourceScopes(trustedPolicy, parsed.resourceScopes, unit.resourceScopes);
9691
+ if (!resourceScopes) {
9692
+ return { status: "rejected", reasonCode: "runtime-scope-conflict" };
9693
+ }
9694
+ const requiredOperations = requiredOperationsFor(trustedPolicy, parsed, resourceScopes, unit.requiredOperations);
9695
+ if (!declarationChanged(unit, requiredOperations, resourceScopes)) {
9696
+ return { status: "rejected", reasonCode: "unit-active" };
9697
+ }
9698
+ unit.requiredOperations = requiredOperations;
9699
+ unit.declaredResourceOperations = Object.freeze([...resourceScopes.keys()]);
9700
+ unit.resourceScopes = resourceScopes;
9701
+ for (const [operation, resource] of resourceScopes) {
9702
+ currentResourceIdentities.set(operation, resource);
9703
+ }
9704
+ return { status: "started", unit: cloneUnit(unit) };
9705
+ }
9617
9706
  function startParsedUnit(parsed, trustedPolicy) {
9618
9707
  if (!epoch)
9619
9708
  return { status: "rejected", reasonCode: "no-active-epoch" };
9620
9709
  if (epoch.unit?.status === "active") {
9621
- return { status: "rejected", reasonCode: "unit-active" };
9710
+ return startActiveUnit(parsed, trustedPolicy, epoch.unit);
9622
9711
  }
9623
9712
  const resourceScopes = mergeResourceScopes(trustedPolicy, parsed.resourceScopes);
9624
9713
  if (!resourceScopes) {
@@ -9706,7 +9795,7 @@ function createWorkflowGuard(options) {
9706
9795
  reasonCode: existing.state === "abandoned" ? "abandoned-transition" : "transition-terminal"
9707
9796
  };
9708
9797
  }
9709
- async function processOperation(input, parsed, unit) {
9798
+ async function processOperation(input, parsed, unit, trustedClassification) {
9710
9799
  if (!unit.requiredOperations.includes(parsed.operation)) {
9711
9800
  markTerminalOperation(parsed, input);
9712
9801
  return { status: "rejected", reasonCode: "operation-not-required" };
@@ -9725,7 +9814,7 @@ function createWorkflowGuard(options) {
9725
9814
  if (prepared.status !== "prepared") {
9726
9815
  return prepared.reasonCode === "call-context-conflict" ? { status: "rejected", reasonCode: "call-context-conflict" } : { status: "rejected", reasonCode: "rejected-operation" };
9727
9816
  }
9728
- const classification = await classifyPreparedOperation(parsed, input, unit);
9817
+ const classification = trustedClassification ?? await classifyPreparedOperation(parsed, input, unit);
9729
9818
  return classification ? finalizeOperation(parsed, input, unit, classification) : { status: "rejected", reasonCode: "guard-unavailable" };
9730
9819
  }
9731
9820
  function rejectParsedOperation(parsed, input, unit, reasonCode) {
@@ -10231,12 +10320,44 @@ function createWorkflowGuard(options) {
10231
10320
  }
10232
10321
  return processOperation(input, parsed, epoch.unit);
10233
10322
  },
10323
+ async observeTrustedRecoveredOperation(input) {
10324
+ const modeResult = evidenceModeResult();
10325
+ if (modeResult)
10326
+ return modeResult;
10327
+ if (!epoch)
10328
+ return { status: "rejected", reasonCode: "no-active-epoch" };
10329
+ if (!epoch.unit)
10330
+ return { status: "rejected", reasonCode: "no-active-unit" };
10331
+ if (epoch.unit.status === "completed") {
10332
+ return { status: "rejected", reasonCode: "unit-completed" };
10333
+ }
10334
+ const terminalResult = terminalOperationResult(input);
10335
+ if (terminalResult)
10336
+ return terminalResult;
10337
+ const parsed = parseReceiptOperationObservation(input);
10338
+ if (!parsed) {
10339
+ markTerminalOperation(undefined, input);
10340
+ return recordGlobalEvidenceIssue("invalid-receipt");
10341
+ }
10342
+ const classification = {
10343
+ outcome: "accepted",
10344
+ category: parsed.operation,
10345
+ attribution: "runtime-verified",
10346
+ result: "success",
10347
+ sideEffect: parsed.operation === "verification" || parsed.operation === "check-readback" || parsed.operation === "review-readback" ? "not-required" : "required",
10348
+ reasonCode: "recognized-command"
10349
+ };
10350
+ return processOperation(input, parsed, epoch.unit, classification);
10351
+ },
10234
10352
  observeReadback(input) {
10235
10353
  const parsed = parseReadback(input);
10236
10354
  if (!parsed)
10237
10355
  return { status: "rejected", reasonCode: "invalid-receipt" };
10238
10356
  return observeRevision(parsed, epoch?.unit);
10239
10357
  },
10358
+ currentOperationContext() {
10359
+ return currentOperationContext();
10360
+ },
10240
10361
  status() {
10241
10362
  return projection();
10242
10363
  },
@@ -11156,6 +11277,7 @@ function createSessionRuntime(options) {
11156
11277
  const rollupKey = `${host.sessionID}:${host.callID}:${childSessionID}`;
11157
11278
  if (rolledUpChildren.has(rollupKey))
11158
11279
  return;
11280
+ const parentBefore = guard.currentOperationContext();
11159
11281
  const children = await options.hostReadback.listChildren(host.sessionID);
11160
11282
  if (!children.some((child) => child.sessionId === childSessionID && child.parentID === host.sessionID)) {
11161
11283
  markUnavailable();
@@ -11203,18 +11325,27 @@ function createSessionRuntime(options) {
11203
11325
  const currentStatus = guard.status();
11204
11326
  if (!currentStatus.epoch || !currentStatus.unit)
11205
11327
  return;
11206
- const expectedWorkspace = ledger.digestIdentity("workspace", options.workspaceIdentity);
11207
- const expectedRepository = ledger.digestIdentity("repository", options.repositoryIdentity ?? current.snapshot.repositoryRevisionDigest);
11208
- const expectedWorktree = ledger.digestIdentity("worktree", options.worktreeIdentity ?? current.snapshot.worktreeRevisionDigest);
11209
- let minted = false;
11328
+ const expectedWorkspace = childLedger.digestIdentity("workspace", parentBefore.workspaceIdentity);
11329
+ const expectedRepository = childLedger.digestIdentity("repository", current.snapshot.repositoryRevisionDigest);
11330
+ const expectedWorktree = childLedger.digestIdentity("worktree", current.snapshot.worktreeRevisionDigest);
11331
+ const candidates = [];
11210
11332
  for (const childReceipt of recovered.receipts) {
11211
11333
  const operation = childReceipt.canonical.operation;
11212
11334
  if (!localOperation(operation))
11213
11335
  continue;
11214
- if (childReceipt.canonical.workspaceDigest !== expectedWorkspace || childReceipt.canonical.repositoryDigest !== expectedRepository || childReceipt.canonical.worktreeDigest !== expectedWorktree) {
11336
+ if (childReceipt.canonical.workspaceDigest !== expectedWorkspace) {
11215
11337
  markUnavailable();
11216
11338
  return;
11217
11339
  }
11340
+ if (childReceipt.canonical.repositoryDigest !== expectedRepository || childReceipt.canonical.worktreeDigest !== expectedWorktree) {
11341
+ continue;
11342
+ }
11343
+ candidates.push(childReceipt);
11344
+ }
11345
+ let minted = false;
11346
+ for (const childReceipt of candidates) {
11347
+ const operation = childReceipt.canonical.operation;
11348
+ const parentContext = guard.currentOperationContext();
11218
11349
  const callID = `task-${host.callID}-${childReceipt.canonical.receiptId}`;
11219
11350
  const observation = {
11220
11351
  callId: callID,
@@ -11223,12 +11354,12 @@ function createSessionRuntime(options) {
11223
11354
  context: {
11224
11355
  epochId: currentStatus.epoch.epochId,
11225
11356
  unitId: currentStatus.unit.unitId,
11226
- workspaceIdentity: options.workspaceIdentity,
11227
- repositoryIdentity: operation === "commit" ? options.repositoryIdentity ?? current.snapshot.repositoryRevisionDigest : current.snapshot.repositoryRevisionDigest,
11228
- worktreeIdentity: operation === "implementation" ? options.worktreeIdentity ?? current.snapshot.worktreeRevisionDigest : current.snapshot.worktreeRevisionDigest
11357
+ workspaceIdentity: parentContext.workspaceIdentity,
11358
+ ...parentContext.repositoryIdentity ? { repositoryIdentity: parentContext.repositoryIdentity } : {},
11359
+ ...parentContext.worktreeIdentity ? { worktreeIdentity: parentContext.worktreeIdentity } : {}
11229
11360
  },
11230
11361
  after: {
11231
- workspaceIdentity: options.workspaceIdentity,
11362
+ workspaceIdentity: parentContext.workspaceIdentity,
11232
11363
  repositoryIdentity: current.snapshot.repositoryRevisionDigest,
11233
11364
  worktreeIdentity: current.snapshot.worktreeRevisionDigest
11234
11365
  },
@@ -11238,7 +11369,7 @@ function createSessionRuntime(options) {
11238
11369
  noOp: false
11239
11370
  }
11240
11371
  };
11241
- const result = await guard.observeOperation(observation);
11372
+ const result = await guard.observeTrustedRecoveredOperation(observation);
11242
11373
  if (result.status === "accepted") {
11243
11374
  const receipt = receiptForOperation(callID, operation);
11244
11375
  if (receipt)
@@ -11433,13 +11564,21 @@ function createSessionRuntime(options) {
11433
11564
  }
11434
11565
  return result;
11435
11566
  }
11567
+ function progressionResourceScopes(unit) {
11568
+ return [...unit.resourceScopes].sort((first, second) => first.operation.localeCompare(second.operation)).map((scope) => ({
11569
+ operation: scope.operation,
11570
+ resourceIdentity: ledger.digestIdentity("resource", scope.resourceIdentity)
11571
+ }));
11572
+ }
11436
11573
  function writeStartResult(output, result) {
11437
11574
  if (!isRecord7(output))
11438
11575
  return;
11576
+ const existingMetadata = isRecord7(output.metadata) ? output.metadata : {};
11439
11577
  if (result.status === "started") {
11440
11578
  output.title = "Workflow unit started";
11441
11579
  output.output = JSON.stringify({ status: "started" });
11442
11580
  output.metadata = {
11581
+ ...existingMetadata,
11443
11582
  ...metadata2(),
11444
11583
  workflowGuard: { status: "started" }
11445
11584
  };
@@ -11452,6 +11591,7 @@ function createSessionRuntime(options) {
11452
11591
  reasonCode
11453
11592
  });
11454
11593
  output.metadata = {
11594
+ ...existingMetadata,
11455
11595
  ...metadata2(),
11456
11596
  workflowGuard: {
11457
11597
  status: "rejected",
@@ -11783,7 +11923,7 @@ function createSessionRuntime(options) {
11783
11923
  unitId: status.unit.unitId,
11784
11924
  family: status.epoch.family,
11785
11925
  requiredOperations: status.unit.requiredOperations,
11786
- resourceScopes: [],
11926
+ resourceScopes: progressionResourceScopes(status.unit),
11787
11927
  state: "started",
11788
11928
  transitionDigest: ledger.digestIdentity("call", host.callID)
11789
11929
  }));
@@ -11840,7 +11980,7 @@ function createSessionRuntime(options) {
11840
11980
  unitId: unit.unitId,
11841
11981
  family: epoch.family,
11842
11982
  requiredOperations: unit.requiredOperations,
11843
- resourceScopes: [],
11983
+ resourceScopes: progressionResourceScopes(unit),
11844
11984
  state: "started",
11845
11985
  transitionDigest: ledger.digestIdentity("call", host.callID)
11846
11986
  }));
@@ -12205,9 +12345,10 @@ function createSessionRuntime(options) {
12205
12345
  const marker = projectReceiptMintMarker(receipt, ledger.getSessionSalt());
12206
12346
  if (!marker)
12207
12347
  return;
12348
+ const existing = output.metadata[SYSTEMATIC_WORKFLOW_RECEIPT_METADATA_KEY];
12208
12349
  output.metadata = {
12209
12350
  ...output.metadata,
12210
- [SYSTEMATIC_WORKFLOW_RECEIPT_METADATA_KEY]: marker
12351
+ [SYSTEMATIC_WORKFLOW_RECEIPT_METADATA_KEY]: existing ? Array.isArray(existing) ? [...existing, marker] : [existing, marker] : marker
12211
12352
  };
12212
12353
  }
12213
12354
  function mergeProgressionMarker(output, marker) {
@@ -1,5 +1,6 @@
1
1
  import { type ReceiptClassifier } from './receipt-classifier.js';
2
2
  import type { ReceiptLedger, ReceiptOperation } from './receipt-ledger.js';
3
+ import type { ReceiptResourceScope } from './receipt-readback.js';
3
4
  export type WorkflowMode = 'protected' | 'disabled' | 'unavailable';
4
5
  export type WorkflowState = 'protected' | 'waiting' | 'rejected' | 'disabled' | 'unavailable';
5
6
  export type RepairKind = 'fresh-readback' | 'rerun-operation' | 'question-attestation';
@@ -34,6 +35,12 @@ export interface UnitSnapshot {
34
35
  status: 'active' | 'completed';
35
36
  requiredOperations: readonly ReceiptOperation[];
36
37
  requiredResourceOperations: readonly ReceiptOperation[];
38
+ resourceScopes: readonly ReceiptResourceScope[];
39
+ }
40
+ export interface CurrentOperationContext {
41
+ readonly workspaceIdentity: string;
42
+ readonly repositoryIdentity?: string;
43
+ readonly worktreeIdentity?: string;
37
44
  }
38
45
  export interface WorkflowStatus {
39
46
  state: WorkflowState;
@@ -125,7 +132,14 @@ export interface WorkflowGuard {
125
132
  observeReceipt(input: unknown): EvidenceObservationResult;
126
133
  observeAttempt(input: unknown): EvidenceObservationResult;
127
134
  observeOperation(input: unknown): Promise<EvidenceObservationResult>;
135
+ /**
136
+ * Internal recovery seam. Callers MUST validate host lineage, own
137
+ * registration/seed/readback, stable workspace, and current mutable
138
+ * revisions before using this classifier-bypassing path.
139
+ */
140
+ observeTrustedRecoveredOperation(input: unknown): Promise<EvidenceObservationResult>;
128
141
  observeReadback(input: unknown): ReadbackObservationResult;
142
+ currentOperationContext(): CurrentOperationContext;
129
143
  status(): WorkflowStatus;
130
144
  prepareTransition(input: unknown): TransitionPrepareResult;
131
145
  finalizeTransition(input: unknown): TransitionFinalizeResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "3.5.0",
3
+ "version": "3.5.1",
4
4
  "description": "Compound-engineering loops for OpenCode, Pi, and Claude Code",
5
5
  "type": "module",
6
6
  "homepage": "https://fro.bot/systematic",
@@ -90,10 +90,10 @@
90
90
  }
91
91
  },
92
92
  "devDependencies": {
93
- "@biomejs/biome": "2.5.5",
93
+ "@biomejs/biome": "2.5.6",
94
94
  "@earendil-works/pi-coding-agent": "0.80.10",
95
- "@opencode-ai/plugin": "1.18.6",
96
- "@opencode-ai/sdk": "1.18.6",
95
+ "@opencode-ai/plugin": "1.18.8",
96
+ "@opencode-ai/sdk": "1.18.8",
97
97
  "@semantic-release/exec": "7.1.0",
98
98
  "@tintinweb/pi-subagents": "0.14.3",
99
99
  "@types/bun": "latest",