@brainervirus/workit-core 8.15.0 → 8.16.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.
- package/package.json +1 -1
- package/src/core/task-contract.ts +21 -0
- package/src/core/task-engine.ts +14 -2
- package/src/core/task-evaluation.ts +71 -0
- package/src/core/workers.ts +9 -0
package/package.json
CHANGED
|
@@ -823,6 +823,27 @@ export const taskSummarySchema = z
|
|
|
823
823
|
})
|
|
824
824
|
.strict();
|
|
825
825
|
export type TaskSummary = z.infer<typeof taskSummarySchema>;
|
|
826
|
+
/** Bounded `inspect` projection (`view: "summary"`): under 4 KB however long the task history is. */
|
|
827
|
+
export type TaskBrief = {
|
|
828
|
+
id: string;
|
|
829
|
+
revision: string;
|
|
830
|
+
workspaceRevision: string;
|
|
831
|
+
updatedAt: string;
|
|
832
|
+
objective: string;
|
|
833
|
+
status: TaskSummary["status"];
|
|
834
|
+
closure: { outcome: Closure["outcome"]; at: string; summary: string } | null;
|
|
835
|
+
progress: {
|
|
836
|
+
summary: string;
|
|
837
|
+
nextAction: string | null;
|
|
838
|
+
blockers: { reason: string; dependentAction: string }[];
|
|
839
|
+
};
|
|
840
|
+
requirements: {
|
|
841
|
+
total: number;
|
|
842
|
+
unmet: number;
|
|
843
|
+
open: { dimension: Dimension; before: string; status: RequirementEvaluation["status"] }[];
|
|
844
|
+
};
|
|
845
|
+
truncated: boolean;
|
|
846
|
+
};
|
|
826
847
|
export const taskListItemSchema = taskSummarySchema
|
|
827
848
|
.omit({ policy: true, requirements: true })
|
|
828
849
|
.extend({ source: provenanceSchema.pick({ host: true, kind: true }) })
|
package/src/core/task-engine.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type Result,
|
|
36
36
|
type Scope,
|
|
37
37
|
type TaskRecord,
|
|
38
|
+
type TaskBrief,
|
|
38
39
|
type TaskListItem,
|
|
39
40
|
type TaskSummary,
|
|
40
41
|
type TaskView,
|
|
@@ -81,6 +82,7 @@ import {
|
|
|
81
82
|
type NativeWorkerVerifier,
|
|
82
83
|
} from "./workers";
|
|
83
84
|
import {
|
|
85
|
+
boundTaskSummary,
|
|
84
86
|
captureCandidate,
|
|
85
87
|
checkPin,
|
|
86
88
|
signalFreshness,
|
|
@@ -115,6 +117,8 @@ export type OperationContext = {
|
|
|
115
117
|
now: Utc | (() => Utc);
|
|
116
118
|
nativeWorker?: NativeWorkerVerifier;
|
|
117
119
|
workerId?: string | null;
|
|
120
|
+
/** A child session with no worker record: it may only read the bounded task summary. */
|
|
121
|
+
readOnlyChild?: boolean;
|
|
118
122
|
};
|
|
119
123
|
|
|
120
124
|
export type ObserveCheckRequest = {
|
|
@@ -872,19 +876,27 @@ export class WorkitCore {
|
|
|
872
876
|
return failure("invalid_input", "unknown state action");
|
|
873
877
|
}
|
|
874
878
|
|
|
875
|
-
task(request: unknown): Result<TaskSummary | TaskListItem[] | TaskView> {
|
|
879
|
+
task(request: unknown): Result<TaskSummary | TaskBrief | TaskListItem[] | TaskView> {
|
|
876
880
|
const resolved = this.implicitTask("task", request);
|
|
877
881
|
if (!resolved.ok) return resolved;
|
|
878
882
|
const value = resolved.data;
|
|
879
883
|
return this.retryOmittedRevisions(value, () => this.taskOnce(value));
|
|
880
884
|
}
|
|
881
885
|
|
|
882
|
-
private taskOnce(request: unknown): Result<TaskSummary | TaskListItem[] | TaskView> {
|
|
886
|
+
private taskOnce(request: unknown): Result<TaskSummary | TaskBrief | TaskListItem[] | TaskView> {
|
|
883
887
|
const root = this.contextRootError();
|
|
884
888
|
if (!root.ok) return root;
|
|
885
889
|
const parsed = parseOperation("task", request);
|
|
886
890
|
if (!parsed.ok) return parsed;
|
|
887
891
|
const input = parsed.data;
|
|
892
|
+
if (this.context.readOnlyChild) {
|
|
893
|
+
if (input.action !== "inspect")
|
|
894
|
+
return failure("permission_denied", "child sessions can only inspect the task summary");
|
|
895
|
+
const task = this.store.readTask(input.taskId);
|
|
896
|
+
if (!task.ok) return task;
|
|
897
|
+
const summary = this.summary(task.data);
|
|
898
|
+
return summary.ok ? { ...summary, data: boundTaskSummary(summary.data) } : summary;
|
|
899
|
+
}
|
|
888
900
|
if ((this.context.workerId ?? null) !== null) {
|
|
889
901
|
if (input.action !== "inspect" && input.action !== "list")
|
|
890
902
|
return failure("permission_denied", "helpers cannot control task lifecycle or scope");
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
type EvidenceEvaluation,
|
|
19
19
|
type Outcome,
|
|
20
20
|
type Requirement,
|
|
21
|
+
type TaskBrief,
|
|
22
|
+
type TaskSummary,
|
|
21
23
|
type Result,
|
|
22
24
|
type Scope,
|
|
23
25
|
type TaskRecord,
|
|
@@ -1074,3 +1076,72 @@ export function evaluateClosure(
|
|
|
1074
1076
|
requirementIds: evaluations.map((item) => item.requirementId),
|
|
1075
1077
|
});
|
|
1076
1078
|
}
|
|
1079
|
+
|
|
1080
|
+
export const TASK_BRIEF_MAX_BYTES = 4096;
|
|
1081
|
+
const BRIEF_TEXT = 250;
|
|
1082
|
+
const BRIEF_BLOCKERS = 3;
|
|
1083
|
+
const BRIEF_OPEN = 8;
|
|
1084
|
+
|
|
1085
|
+
/** Cut `value` until its JSON-escaped form fits `BRIEF_TEXT` bytes. */
|
|
1086
|
+
const clip = (value: string): string => {
|
|
1087
|
+
const fits = (text: string) => Buffer.byteLength(JSON.stringify(text)) <= BRIEF_TEXT;
|
|
1088
|
+
if (fits(value)) return value;
|
|
1089
|
+
let length = Math.min(value.length, BRIEF_TEXT);
|
|
1090
|
+
while (length > 0 && !fits(`${value.slice(0, length)}…`)) length -= 1;
|
|
1091
|
+
return `${value.slice(0, length)}…`;
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Bound a task summary for `inspect --view summary`: text is clipped, lists
|
|
1096
|
+
* capped and the policy reduced to the requirements still owed, so the JSON
|
|
1097
|
+
* stays under {@link TASK_BRIEF_MAX_BYTES} however long the task history is.
|
|
1098
|
+
*/
|
|
1099
|
+
export function boundTaskSummary(summary: TaskSummary): TaskBrief {
|
|
1100
|
+
const rules = new Map((summary.policy?.requirements ?? []).map((item) => [item.id, item]));
|
|
1101
|
+
const unmet = summary.requirements.filter(
|
|
1102
|
+
(item) => item.status === "unsatisfied" || item.status === "unavailable",
|
|
1103
|
+
);
|
|
1104
|
+
const { progress, closure } = summary;
|
|
1105
|
+
const next = progress.nextAction;
|
|
1106
|
+
const open = unmet.slice(0, BRIEF_OPEN).map((item) => ({
|
|
1107
|
+
dimension: rules.get(item.requirementId)?.dimension ?? "verification",
|
|
1108
|
+
before: rules.get(item.requirementId)?.before ?? "close",
|
|
1109
|
+
status: item.status,
|
|
1110
|
+
}));
|
|
1111
|
+
const brief: TaskBrief = {
|
|
1112
|
+
id: summary.id,
|
|
1113
|
+
revision: summary.revision,
|
|
1114
|
+
workspaceRevision: summary.workspaceRevision,
|
|
1115
|
+
updatedAt: summary.updatedAt,
|
|
1116
|
+
objective: clip(summary.objective),
|
|
1117
|
+
status: summary.status,
|
|
1118
|
+
closure: closure && {
|
|
1119
|
+
outcome: closure.outcome,
|
|
1120
|
+
at: closure.at,
|
|
1121
|
+
summary: clip(closure.summary),
|
|
1122
|
+
},
|
|
1123
|
+
progress: {
|
|
1124
|
+
summary: clip(progress.summary),
|
|
1125
|
+
nextAction: next && clip(next),
|
|
1126
|
+
blockers: progress.blockers.slice(0, BRIEF_BLOCKERS).map((item) => ({
|
|
1127
|
+
reason: clip(item.reason),
|
|
1128
|
+
dependentAction: clip(item.dependentAction),
|
|
1129
|
+
})),
|
|
1130
|
+
},
|
|
1131
|
+
requirements: { total: summary.requirements.length, unmet: unmet.length, open },
|
|
1132
|
+
truncated: false,
|
|
1133
|
+
};
|
|
1134
|
+
brief.truncated =
|
|
1135
|
+
JSON.stringify(brief).includes("…") ||
|
|
1136
|
+
progress.blockers.length > BRIEF_BLOCKERS ||
|
|
1137
|
+
unmet.length > BRIEF_OPEN;
|
|
1138
|
+
return brief;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/** For a child or unattested caller: `task inspect` with the summary view returns the brief. */
|
|
1142
|
+
export function briefInspectResult(request: unknown, result: Result<unknown>): Result<unknown> {
|
|
1143
|
+
const { action, view } = (request ?? {}) as { action?: unknown; view?: unknown };
|
|
1144
|
+
return result.ok && action === "inspect" && (view ?? "summary") === "summary"
|
|
1145
|
+
? { ...result, data: boundTaskSummary(result.data as TaskSummary) }
|
|
1146
|
+
: result;
|
|
1147
|
+
}
|
package/src/core/workers.ts
CHANGED
|
@@ -51,3 +51,12 @@ export const workerBlocksTransition = (
|
|
|
51
51
|
transition: "pause" | "resume" | "revise" | "close",
|
|
52
52
|
state: WorkerState,
|
|
53
53
|
): boolean => (transition === "pause" ? false : isUncertainWorker(state));
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A worker may be continued in its own session only while it holds live
|
|
57
|
+
* state: a running process, or uncommitted changes in its worktree. A worker
|
|
58
|
+
* that reported and is clean is continued by a fresh worker on the same
|
|
59
|
+
* slice. An unreadable worktree counts as no live state.
|
|
60
|
+
*/
|
|
61
|
+
export const holdsLiveState = (state: WorkerState, dirty: boolean | null): boolean =>
|
|
62
|
+
state === "running" || dirty === true;
|