@fro.bot/systematic 3.5.0 → 3.5.2
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 +164 -23
- package/dist/lib/workflow-guard.d.ts +14 -0
- package/package.json +4 -4
- package/skills/orchestrating-subagents/SKILL.md +10 -0
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
11207
|
-
const expectedRepository =
|
|
11208
|
-
const expectedWorktree =
|
|
11209
|
-
|
|
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
|
|
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:
|
|
11227
|
-
repositoryIdentity
|
|
11228
|
-
worktreeIdentity
|
|
11357
|
+
workspaceIdentity: parentContext.workspaceIdentity,
|
|
11358
|
+
...parentContext.repositoryIdentity ? { repositoryIdentity: parentContext.repositoryIdentity } : {},
|
|
11359
|
+
...parentContext.worktreeIdentity ? { worktreeIdentity: parentContext.worktreeIdentity } : {}
|
|
11229
11360
|
},
|
|
11230
11361
|
after: {
|
|
11231
|
-
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.
|
|
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.
|
|
3
|
+
"version": "3.5.2",
|
|
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.
|
|
93
|
+
"@biomejs/biome": "2.5.6",
|
|
94
94
|
"@earendil-works/pi-coding-agent": "0.80.10",
|
|
95
|
-
"@opencode-ai/plugin": "1.18.
|
|
96
|
-
"@opencode-ai/sdk": "1.18.
|
|
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",
|
|
@@ -17,6 +17,7 @@ Every subagent dispatch should carry the same bounded brief, expressed through t
|
|
|
17
17
|
Specialist: systematic-implementer
|
|
18
18
|
Objective: Implement the auth module
|
|
19
19
|
Scope: Make only the changes needed for the auth module.
|
|
20
|
+
Workspace: /absolute/path/to/expected-repository-or-worktree
|
|
20
21
|
Return: List the changed files and summarize the implementation.
|
|
21
22
|
```
|
|
22
23
|
|
|
@@ -24,6 +25,7 @@ Return: List the changed files and summarize the implementation.
|
|
|
24
25
|
- **Specialist** — the registered agent or persona best suited to the work (for example, an implementer or researcher)
|
|
25
26
|
- **Objective** — a concise statement of the outcome
|
|
26
27
|
- **Scope** — full instructions, boundaries, dependencies, and files in scope
|
|
28
|
+
- **Workspace** — for any write-capable dispatch, the explicit expected repository or worktree root
|
|
27
29
|
- **Return** — the result format expected from the specialist
|
|
28
30
|
|
|
29
31
|
**Optional brief elements:**
|
|
@@ -61,6 +63,7 @@ After the first result, dispatch:
|
|
|
61
63
|
Objective: Implement caching
|
|
62
64
|
Scope: Implement caching based on the research result below.
|
|
63
65
|
Input: Include the first specialist's returned research.
|
|
66
|
+
Workspace: /absolute/path/to/expected-repository-or-worktree
|
|
64
67
|
Return: List changed files and summarize the implementation.
|
|
65
68
|
```
|
|
66
69
|
|
|
@@ -110,6 +113,12 @@ When dispatching units in parallel, include these instructions in each specialis
|
|
|
110
113
|
|
|
111
114
|
> Do not stage files (`git add`), create commits, or run the project test suite. Leave staging and committing to the orchestrator or caller.
|
|
112
115
|
|
|
116
|
+
## Workspace Attestation
|
|
117
|
+
|
|
118
|
+
For every write-capable dispatch, the specialist must verify and return its actual repository or worktree root using available repository tooling (for example, `git rev-parse --show-toplevel` when Git is available) and an inventory of changed files. The orchestrator must independently verify the expected worktree's status and diff, plus the source or primary checkout's status, before accepting the result; a path in the brief or a specialist's self-report is not evidence.
|
|
119
|
+
|
|
120
|
+
If writes landed in the wrong checkout, stop dependent dispatches, inspect both diffs, transfer only intended changes deliberately, and restore only confirmed accidental agent edits. Do not blindly copy or reset.
|
|
121
|
+
|
|
113
122
|
## Result Synthesis
|
|
114
123
|
|
|
115
124
|
After specialists complete, the orchestrator synthesizes results:
|
|
@@ -138,6 +147,7 @@ After specialists complete, the orchestrator synthesizes results:
|
|
|
138
147
|
| Background unavailable | Foreground only — serial or batched |
|
|
139
148
|
| Specialist fails | Diagnose, correct the brief, then re-dispatch or resume the prior session where supported |
|
|
140
149
|
| File collision detected post-parallel | Stage non-colliding files (if workflow owns git ops), re-run colliding units serially |
|
|
150
|
+
| Write-capable dispatch | Include the expected root; require actual-root and changed-file attestation; independently verify both worktrees before acceptance |
|
|
141
151
|
|
|
142
152
|
## Common Mistakes
|
|
143
153
|
|