@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "8.15.0",
3
+ "version": "8.16.0",
4
4
  "private": false,
5
5
  "description": "Workit shared core — task, policy, evidence, review, decision, worker, and writer state for agentic coding workflows",
6
6
  "keywords": [
@@ -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 }) })
@@ -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
+ }
@@ -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;