@brainervirus/workit-core 1.0.10 → 1.0.12
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/package.json +1 -1
- package/src/core/authority.ts +53 -11
- package/src/core/branch.ts +39 -2
- package/src/core/doctor.ts +25 -12
- package/src/core/external-action-effects.ts +107 -24
- package/src/core/external-action.ts +343 -27
- package/src/core/methods.ts +8 -8
- package/src/core/route-intent.ts +28 -0
- package/src/core/runtime-identity.ts +33 -0
- package/src/core/task-contract.ts +109 -1
- package/src/core/task-engine.ts +207 -240
- package/src/core/task-evaluation.ts +129 -24
- package/src/core/task-store.ts +4 -6
- package/src/core/workers.ts +11 -1
- package/src/core.ts +15 -1
|
@@ -377,6 +377,52 @@ export function evaluateEvidence(
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
export const scopeCovers = contractScopeCovers;
|
|
380
|
+
|
|
381
|
+
/** One shared pin check for evidence and finding records: an unpinned
|
|
382
|
+
* assignment accepts anything in scope; a pinned one accepts the pin on
|
|
383
|
+
* either candidate slot. Absent IDs behave as null. */
|
|
384
|
+
export const checkPin = (
|
|
385
|
+
assignment: { candidateId?: string | null },
|
|
386
|
+
beforeCandidateId: string | null | undefined,
|
|
387
|
+
candidateId: string | null | undefined,
|
|
388
|
+
): boolean => {
|
|
389
|
+
const pin = assignment.candidateId ?? null;
|
|
390
|
+
if (pin === null) return true;
|
|
391
|
+
return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/** Resolve effective evidence binding: explicit IDs win; otherwise a pinned
|
|
395
|
+
* worker defaults to its pin and everyone else to the current candidate
|
|
396
|
+
* (checks and reviews) or null. Callers validate and pin-check the resolved
|
|
397
|
+
* values — never the caller's possibly-empty fields. */
|
|
398
|
+
export const resolveBinding = (
|
|
399
|
+
evidence: {
|
|
400
|
+
beforeCandidateId?: string | null;
|
|
401
|
+
candidateId?: string | null;
|
|
402
|
+
kind: string;
|
|
403
|
+
},
|
|
404
|
+
pin: string | null | undefined,
|
|
405
|
+
currentCandidateId: string | null,
|
|
406
|
+
): { beforeCandidateId: string | null; candidateId: string | null } => {
|
|
407
|
+
const bound = evidence.kind === "check" || evidence.kind === "review";
|
|
408
|
+
const fallback = bound ? (pin ?? currentCandidateId) : null;
|
|
409
|
+
return {
|
|
410
|
+
beforeCandidateId: evidence.beforeCandidateId ?? fallback,
|
|
411
|
+
candidateId: evidence.candidateId ?? fallback,
|
|
412
|
+
};
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/** One shared verification predicate for fixed findings: a passed
|
|
416
|
+
* check/review counts when the finding is unscoped or the evidence is
|
|
417
|
+
* bound to the finding's candidate. Used by the auto-reopen scan, the
|
|
418
|
+
* resolve gate, and closure — one predicate, one error code each. */
|
|
419
|
+
export const findingVerificationPasses = (
|
|
420
|
+
findingCandidateId: string | null | undefined,
|
|
421
|
+
evidence: { kind: string; candidateId: string | null | undefined; status: string },
|
|
422
|
+
): boolean =>
|
|
423
|
+
evidence.status === "passed" &&
|
|
424
|
+
(evidence.kind === "check" || evidence.kind === "review") &&
|
|
425
|
+
((findingCandidateId ?? null) === null || evidence.candidateId === findingCandidateId);
|
|
380
426
|
const applicableDecision = (
|
|
381
427
|
task: TaskRecord,
|
|
382
428
|
workspace: WorkspaceRecord,
|
|
@@ -413,7 +459,7 @@ const applicableRequirementDecision = (
|
|
|
413
459
|
({ data, provenance }) =>
|
|
414
460
|
provenance.kind !== "imported" &&
|
|
415
461
|
data.purpose !== "limitation" &&
|
|
416
|
-
data.response === "approved" &&
|
|
462
|
+
(data.response === "approved" || data.response === "stated") &&
|
|
417
463
|
data.revoked === null &&
|
|
418
464
|
data.binding.taskId === task.id &&
|
|
419
465
|
data.binding.workspaceId === workspace.id &&
|
|
@@ -430,9 +476,11 @@ const evidenceMatchesRequirement = (
|
|
|
430
476
|
kind: EvidenceEvaluation["status"],
|
|
431
477
|
evidenceKind: string,
|
|
432
478
|
dimension: Requirement["dimension"],
|
|
479
|
+
ruleId?: string,
|
|
433
480
|
): boolean => {
|
|
434
481
|
if (kind !== "passed") return false;
|
|
435
|
-
if (dimension === "testing" || dimension === "verification")
|
|
482
|
+
if (dimension === "testing" || dimension === "verification")
|
|
483
|
+
return evidenceKind === "check" || (ruleId === "pre-pr-cleanup" && evidenceKind === "artifact");
|
|
436
484
|
if (dimension === "review") return evidenceKind === "review";
|
|
437
485
|
if (dimension === "investigation" || dimension === "challenge")
|
|
438
486
|
return evidenceKind === "investigation";
|
|
@@ -440,6 +488,19 @@ const evidenceMatchesRequirement = (
|
|
|
440
488
|
return false;
|
|
441
489
|
};
|
|
442
490
|
|
|
491
|
+
/** Human-readable evidence kinds each requirement dimension accepts, used
|
|
492
|
+
* so kind mismatches fail with the expected kind instead of silence. */
|
|
493
|
+
const expectedKinds = (dimension: Requirement["dimension"], ruleId?: string): string => {
|
|
494
|
+
if (dimension === "testing") return "check";
|
|
495
|
+
if (dimension === "verification")
|
|
496
|
+
return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
|
|
497
|
+
if (dimension === "review") return "review";
|
|
498
|
+
if (dimension === "investigation" || dimension === "challenge") return "investigation";
|
|
499
|
+
if (dimension === "artifacts" || dimension === "continuity") return "artifact";
|
|
500
|
+
if (dimension === "decisions") return "decision";
|
|
501
|
+
return "worker report";
|
|
502
|
+
};
|
|
503
|
+
|
|
443
504
|
export function evaluateRequirements(
|
|
444
505
|
task: TaskRecord,
|
|
445
506
|
workspace: WorkspaceRecord,
|
|
@@ -462,7 +523,14 @@ export function evaluateRequirements(
|
|
|
462
523
|
.map((entry, index) => ({ entry, evaluation: evidence[index] }))
|
|
463
524
|
.filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
|
|
464
525
|
const passed = related.filter(({ entry, evaluation }) => {
|
|
465
|
-
if (
|
|
526
|
+
if (
|
|
527
|
+
!evidenceMatchesRequirement(
|
|
528
|
+
evaluation.status,
|
|
529
|
+
entry.data.kind,
|
|
530
|
+
requirement.dimension,
|
|
531
|
+
requirement.ruleId,
|
|
532
|
+
)
|
|
533
|
+
)
|
|
466
534
|
return false;
|
|
467
535
|
if (
|
|
468
536
|
(entry.data.kind === "check" || entry.data.kind === "review") &&
|
|
@@ -474,10 +542,15 @@ export function evaluateRequirements(
|
|
|
474
542
|
if (requirement.dimension !== "review") return true;
|
|
475
543
|
const reviewSession = sessionFromRef(entry.data.reviewContext);
|
|
476
544
|
const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
|
|
477
|
-
|
|
478
|
-
|
|
545
|
+
// Reviewer-session uniqueness is per candidate, not per task: the
|
|
546
|
+
// same session may re-verify after the candidate changes, but never
|
|
547
|
+
// manufacture two reviews for one candidate.
|
|
548
|
+
const sameEvidenceSession = related.some(
|
|
549
|
+
({ entry: other }) =>
|
|
479
550
|
other.id !== entry.id &&
|
|
480
551
|
other.data.kind === "review" &&
|
|
552
|
+
other.data.candidateId !== null &&
|
|
553
|
+
other.data.candidateId === entry.data.candidateId &&
|
|
481
554
|
sameSession(reviewSession, other.provenance.session),
|
|
482
555
|
);
|
|
483
556
|
return (
|
|
@@ -488,19 +561,34 @@ export function evaluateRequirements(
|
|
|
488
561
|
);
|
|
489
562
|
});
|
|
490
563
|
if (passed.length) {
|
|
491
|
-
// RED-first
|
|
492
|
-
//
|
|
493
|
-
//
|
|
564
|
+
// RED-first, scoped to fresh claims: a testing pass bound to the
|
|
565
|
+
// current candidate needs a preceding recorded failure, unless a
|
|
566
|
+
// passing baseline already exists from earlier work — a check that
|
|
567
|
+
// passed (when recorded) against a different candidate. Stale passes
|
|
568
|
+
// count as baselines: green on an older tree is exactly what "prior
|
|
569
|
+
// passing baseline" means. A GREEN with neither is indistinguishable
|
|
570
|
+
// from code-then-tests.
|
|
571
|
+
const currentId = candidate?.id ?? null;
|
|
494
572
|
const withRed =
|
|
495
573
|
requirement.dimension === "testing"
|
|
496
|
-
? passed.filter(
|
|
497
|
-
|
|
498
|
-
(
|
|
499
|
-
other
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
574
|
+
? passed.filter(
|
|
575
|
+
({ entry }) =>
|
|
576
|
+
related.some(
|
|
577
|
+
(other) =>
|
|
578
|
+
other.entry.data.kind === "check" &&
|
|
579
|
+
other.evaluation.status === "failed" &&
|
|
580
|
+
other.entry.data.requirementIds.includes(requirement.id) &&
|
|
581
|
+
other.entry.recordedAt <= entry.recordedAt,
|
|
582
|
+
) ||
|
|
583
|
+
related.some(
|
|
584
|
+
(baseline) =>
|
|
585
|
+
baseline.entry.id !== entry.id &&
|
|
586
|
+
baseline.entry.data.kind === "check" &&
|
|
587
|
+
baseline.entry.data.result === "passed" &&
|
|
588
|
+
baseline.entry.data.candidateId !== null &&
|
|
589
|
+
currentId !== null &&
|
|
590
|
+
baseline.entry.data.candidateId !== currentId,
|
|
591
|
+
),
|
|
504
592
|
)
|
|
505
593
|
: passed;
|
|
506
594
|
if (withRed.length)
|
|
@@ -553,7 +641,7 @@ export function evaluateRequirements(
|
|
|
553
641
|
status: "satisfied" as const,
|
|
554
642
|
evidenceIds: [],
|
|
555
643
|
decisionIds: decisions.map((decision) => decision.id),
|
|
556
|
-
reason: "an applicable approved decision satisfies the requirement",
|
|
644
|
+
reason: "an applicable approved or stated decision satisfies the requirement",
|
|
557
645
|
};
|
|
558
646
|
const limitations = requirement.acceptanceAllowed
|
|
559
647
|
? applicableDecision(task, workspace, requirement, checkoutRoot)
|
|
@@ -573,6 +661,14 @@ export function evaluateRequirements(
|
|
|
573
661
|
capability.assurance === "unavailable" &&
|
|
574
662
|
(capability.name === requirement.dimension || capability.surface === requirement.dimension),
|
|
575
663
|
);
|
|
664
|
+
if (!unavailable && related.length > 0 && !passed.length)
|
|
665
|
+
return {
|
|
666
|
+
requirementId: requirement.id,
|
|
667
|
+
status: "unsatisfied" as const,
|
|
668
|
+
evidenceIds: related.map(({ entry }) => entry.id),
|
|
669
|
+
decisionIds: [],
|
|
670
|
+
reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`,
|
|
671
|
+
};
|
|
576
672
|
return {
|
|
577
673
|
requirementId: requirement.id,
|
|
578
674
|
status: unavailable ? ("unavailable" as const) : ("unsatisfied" as const),
|
|
@@ -614,10 +710,12 @@ export function evaluateClosure(
|
|
|
614
710
|
if (entry.data.disposition === "fixed") {
|
|
615
711
|
const verified = entry.data.resolution?.evidenceIds.some((id) => {
|
|
616
712
|
const evidence = view.task.evidence.find((item) => item.id === id);
|
|
617
|
-
return
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
713
|
+
if (!evidence) return false;
|
|
714
|
+
return findingVerificationPasses(entry.data.candidateId, {
|
|
715
|
+
kind: evidence.data.kind,
|
|
716
|
+
candidateId: evidence.data.candidateId,
|
|
717
|
+
status: evidenceById.get(id) ?? "missing",
|
|
718
|
+
});
|
|
621
719
|
});
|
|
622
720
|
if (!verified)
|
|
623
721
|
return failure("requirements_unsatisfied", "fixed findings require current verification");
|
|
@@ -652,9 +750,16 @@ export function evaluateClosure(
|
|
|
652
750
|
}
|
|
653
751
|
}
|
|
654
752
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
753
|
+
// Close is gated only by close-timed requirements. Dependent-action gates
|
|
754
|
+
// (e.g. deslop before hosting.pull_request) block their named action at
|
|
755
|
+
// reservation time, never the close itself.
|
|
756
|
+
const blocking = evaluations.filter((item) => {
|
|
757
|
+
if (item.status !== "unsatisfied" && item.status !== "unavailable") return false;
|
|
758
|
+
const requirement = view.task.policy?.requirements.find(
|
|
759
|
+
(candidate) => candidate.id === item.requirementId,
|
|
760
|
+
);
|
|
761
|
+
return !requirement || requirement.before === "close";
|
|
762
|
+
});
|
|
658
763
|
if (requestedOutcome !== "stopped" && blocking.length)
|
|
659
764
|
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
660
765
|
requirementIds: blocking.map((item) => item.requirementId),
|
package/src/core/task-store.ts
CHANGED
|
@@ -41,8 +41,7 @@ export type WorkspaceMutation = (
|
|
|
41
41
|
) => Result<WorkspaceRecord>;
|
|
42
42
|
export type CoupledMutation = {
|
|
43
43
|
taskId: Id;
|
|
44
|
-
|
|
45
|
-
expectedRevision?: Revision;
|
|
44
|
+
expectedRevision: Revision;
|
|
46
45
|
expectedWorkspaceRevision: Revision;
|
|
47
46
|
now?: Utc;
|
|
48
47
|
task: TaskMutation;
|
|
@@ -514,10 +513,9 @@ export class TaskStore {
|
|
|
514
513
|
const workspace = this.readWorkspace();
|
|
515
514
|
if (!workspace.ok) return workspace;
|
|
516
515
|
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
517
|
-
const
|
|
518
|
-
if (!
|
|
519
|
-
if (task.data.revision !==
|
|
520
|
-
return this.conflict(expectedTaskRevision, task.data.revision);
|
|
516
|
+
const expected = input.expectedRevision;
|
|
517
|
+
if (!expected) return failure("invalid_input", "task revision is required");
|
|
518
|
+
if (task.data.revision !== expected) return this.conflict(expected, task.data.revision);
|
|
521
519
|
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
522
520
|
return this.conflict(input.expectedWorkspaceRevision, workspace.data.revision);
|
|
523
521
|
const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
|
package/src/core/workers.ts
CHANGED
|
@@ -96,9 +96,19 @@ const same = (left: unknown, right: unknown): boolean => {
|
|
|
96
96
|
/** The one shared definition of active or uncertain worker state used by
|
|
97
97
|
* close, revise, resume, and replacement-launch checks. */
|
|
98
98
|
export const UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"] as const;
|
|
99
|
-
export const isUncertainWorker = (state:
|
|
99
|
+
export const isUncertainWorker = (state: WorkerState): boolean =>
|
|
100
100
|
(UNCERTAIN_WORKER_STATES as readonly string[]).includes(state);
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Lifecycle gate table: which worker states block each lead transition.
|
|
104
|
+
* Pause freezes, so live workers never block it (writer release stays a
|
|
105
|
+
* separate explicit step); resume, revise, and close must reconcile first.
|
|
106
|
+
*/
|
|
107
|
+
export const workerBlocksTransition = (
|
|
108
|
+
transition: "pause" | "resume" | "revise" | "close",
|
|
109
|
+
state: WorkerState,
|
|
110
|
+
): boolean => (transition === "pause" ? false : isUncertainWorker(state));
|
|
111
|
+
|
|
102
112
|
const callerSession = (caller: CallerContext): HostSession =>
|
|
103
113
|
caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
104
114
|
|
package/src/core.ts
CHANGED
|
@@ -22,6 +22,7 @@ export {
|
|
|
22
22
|
decisionDigest,
|
|
23
23
|
candidateDigest,
|
|
24
24
|
requirementId,
|
|
25
|
+
rewriteRecordRefs,
|
|
25
26
|
BINDING_QUESTION_BUDGET,
|
|
26
27
|
workitBindingQuestionIssue,
|
|
27
28
|
} from "./core/task-contract";
|
|
@@ -55,6 +56,7 @@ export type {
|
|
|
55
56
|
TaskView,
|
|
56
57
|
TaskSummary,
|
|
57
58
|
ExportBundle,
|
|
59
|
+
RefMap,
|
|
58
60
|
Result as ContractResult,
|
|
59
61
|
} from "./core/task-contract";
|
|
60
62
|
export { TaskStore, runtimeVersion } from "./core/task-store";
|
|
@@ -68,9 +70,14 @@ export type {
|
|
|
68
70
|
} from "./core/task-context";
|
|
69
71
|
export { METHODS, invariantBootstrap, selectMethods } from "./core/methods";
|
|
70
72
|
export type { MethodId, SelectedMethod } from "./core/methods";
|
|
71
|
-
export { shellRouteIntent } from "./core/route-intent";
|
|
73
|
+
export { hasLiveWorkitTask, shellRouteIntent, shouldDenyShellRoute } from "./core/route-intent";
|
|
74
|
+
export { classifyBranchDirt } from "./core/branch";
|
|
75
|
+
export type { BranchDirt } from "./core/branch";
|
|
76
|
+
export { bundleHashOfFile, isEphemeralCachePath, sha256Hex } from "./core/runtime-identity";
|
|
72
77
|
export type { ShellRouteIntent } from "./core/route-intent";
|
|
73
78
|
export { applicableDecision, reserveAction, settleAction, reconcileAction } from "./core/authority";
|
|
79
|
+
export { normalizeChainSteps, chainStepKey } from "./core/authority";
|
|
80
|
+
export type { ChainStep } from "./core/authority";
|
|
74
81
|
export {
|
|
75
82
|
createAuthorizedExternalActionRunner,
|
|
76
83
|
runAuthorizedExternalAction,
|
|
@@ -86,8 +93,12 @@ export type {
|
|
|
86
93
|
ExternalActionOperation,
|
|
87
94
|
} from "./core/external-action";
|
|
88
95
|
export {
|
|
96
|
+
approvedBranchSetupIntent,
|
|
97
|
+
approvedChainStep,
|
|
89
98
|
approvedExternalAction,
|
|
90
99
|
approvedPlanCommit,
|
|
100
|
+
branchSetupIntent,
|
|
101
|
+
chainStepBinding,
|
|
91
102
|
planCommitDescriptor,
|
|
92
103
|
planCommitBinding,
|
|
93
104
|
commitMessageFromDescriptor,
|
|
@@ -115,9 +126,12 @@ export type {
|
|
|
115
126
|
} from "./core/authority";
|
|
116
127
|
export {
|
|
117
128
|
captureCandidate,
|
|
129
|
+
checkPin,
|
|
118
130
|
evaluateEvidence,
|
|
119
131
|
evaluateRequirements,
|
|
120
132
|
evaluateClosure,
|
|
133
|
+
findingVerificationPasses,
|
|
134
|
+
resolveBinding,
|
|
121
135
|
} from "./core/task-evaluation";
|
|
122
136
|
export type { CandidateEnvironment, ClosureEvaluation } from "./core/task-evaluation";
|
|
123
137
|
export { WorkitCore } from "./core/task-engine";
|