@brainervirus/workit-core 8.14.0 → 8.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "8.14.0",
3
+ "version": "8.14.1",
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": [
@@ -33,6 +33,9 @@ const text = z.string().check((ctx) => {
33
33
  ctx.issues.push({ code: "custom", input: ctx.value, message: "invalid Unicode", path: [] });
34
34
  });
35
35
  const id = text.regex(uuidPattern, "expected lowercase UUID");
36
+ /** A full id or an unambiguous lowercase prefix of one (the 8-char short id other surfaces print). */
37
+ export const idRefPattern = /^[0-9a-f][0-9a-f-]{7,35}$/;
38
+ const idRef = text.regex(idRefPattern, "expected a UUID or an id prefix of 8+ characters");
36
39
  const digest = text.regex(digestPattern, "expected lowercase SHA-256 digest");
37
40
  const revision = text.regex(uuidPattern, "expected UUID revision");
38
41
  const validUtc = (value: string): boolean => {
@@ -598,7 +601,7 @@ export const workerReportSchema = z
598
601
  outcome: z.enum(["completed", "failed", "cancelled"]),
599
602
  summary: text,
600
603
  evidenceIds: z.array(id),
601
- findingIds: z.array(id),
604
+ findingIds: z.array(idRef),
602
605
  })
603
606
  .strict();
604
607
  export type WorkerReport = z.infer<typeof workerReportSchema>;
@@ -938,7 +941,7 @@ const findingOperations = {
938
941
  action: z.literal("resolve"),
939
942
  ...taskId,
940
943
  expectedRevision: revision.optional(),
941
- findingId: id,
944
+ findingId: idRef,
942
945
  disposition: z.enum(["open", "fixed", "dismissed", "deferred"]),
943
946
  reason: text,
944
947
  evidenceIds: z.array(id),
@@ -980,7 +983,7 @@ const workerOperations = {
980
983
  ...taskId,
981
984
  expectedRevision: revision.optional(),
982
985
  expectedWorkspaceRevision: revision.optional(),
983
- workerId: id,
986
+ workerId: idRef,
984
987
  report: workerReportSchema,
985
988
  }),
986
989
  cancel: operation({
@@ -988,7 +991,7 @@ const workerOperations = {
988
991
  ...taskId,
989
992
  ...revisions,
990
993
  expectedWorkspaceRevision: revision.optional(),
991
- workerId: id,
994
+ workerId: idRef,
992
995
  reason: text,
993
996
  }),
994
997
  };
@@ -1118,6 +1121,21 @@ export const failure = (
1118
1121
  details: ErrorDetails = {},
1119
1122
  ): Result<never> => ({ ok: false, schemaVersion: 1, code, error, details });
1120
1123
 
1124
+ /**
1125
+ * Resolve a full id or an unambiguous prefix against the ids a task holds.
1126
+ * An exact match always wins; several prefix matches fail listing the candidates.
1127
+ */
1128
+ export function resolveIdRef(kind: string, ref: string, ids: readonly string[]): Result<string> {
1129
+ if (ids.includes(ref)) return success(null, null, ref);
1130
+ const matches = ids.filter((candidate) => candidate.startsWith(ref));
1131
+ if (matches.length === 1) return success(null, null, matches[0]);
1132
+ if (matches.length === 0) return failure("not_found", `${kind} not found`);
1133
+ return failure(
1134
+ "invalid_input",
1135
+ `${kind} prefix ${ref} is ambiguous; candidates: ${matches.join(", ")}`,
1136
+ );
1137
+ }
1138
+
1121
1139
  export function parseOperation(family: OperationFamily, input: unknown): Result<OperationRequest> {
1122
1140
  if (
1123
1141
  typeof input === "object" &&
@@ -15,6 +15,7 @@ import {
15
15
  legacyAssessmentSchema,
16
16
  newId,
17
17
  parseOperation,
18
+ resolveIdRef,
18
19
  rewriteRecordRefs,
19
20
  success,
20
21
  type Candidate,
@@ -391,6 +392,8 @@ const TASK_SCOPED_ACTIONS: ReadonlySet<string> = new Set([
391
392
  "state.export",
392
393
  ]);
393
394
  /** Worker actions that name their worker: they resolve to that worker's task. */
395
+ const ONLY_OWN_REPORT = "workers can submit only their own report";
396
+ const NO_HELPER_CONTROL = "helpers cannot cancel or control workers";
394
397
  const WORKER_ADDRESSED_ACTIONS: ReadonlySet<string> = new Set(["worker.cancel", "worker.report"]);
395
398
  /**
396
399
  * The exact CLI command that runs worker `request` (cancel or report, with a
@@ -1524,6 +1527,13 @@ export class WorkitCore {
1524
1527
  return success(changed.data.revision, null, changed.data.findings.at(-1)!);
1525
1528
  }
1526
1529
  if (helper?.ok) return failure("permission_denied", "helpers cannot resolve findings");
1530
+ const findingRef = resolveIdRef(
1531
+ "finding",
1532
+ input.findingId,
1533
+ task.data.findings.map((entry) => entry.id),
1534
+ );
1535
+ if (!findingRef.ok) return findingRef;
1536
+ input.findingId = findingRef.data;
1527
1537
  const finding = task.data.findings.find((entry) => entry.id === input.findingId);
1528
1538
  if (!finding) return failure("not_found", "finding not found");
1529
1539
  if (!input.reason.trim())
@@ -1738,13 +1748,39 @@ export class WorkitCore {
1738
1748
  );
1739
1749
  }
1740
1750
 
1751
+ // A helper may only address itself: resolving against its own id alone keeps
1752
+ // another worker's id out of any ambiguity error.
1753
+ const workerRef = resolveIdRef(
1754
+ "worker",
1755
+ input.workerId,
1756
+ helperId === null ? task.data.workers.map((candidate) => candidate.id) : [helperId],
1757
+ );
1758
+ if (!workerRef.ok)
1759
+ return helperId === null
1760
+ ? workerRef
1761
+ : failure(
1762
+ "permission_denied",
1763
+ input.action === "report" ? ONLY_OWN_REPORT : NO_HELPER_CONTROL,
1764
+ );
1765
+ input.workerId = workerRef.data;
1741
1766
  const entry = task.data.workers.find((candidate) => candidate.id === input.workerId);
1742
1767
  if (!entry) return failure("not_found", "worker not found");
1743
1768
  if (input.action === "report") {
1744
1769
  if (helperId === null || helperId !== input.workerId)
1745
- return failure("permission_denied", "workers can submit only their own report");
1770
+ return failure("permission_denied", ONLY_OWN_REPORT);
1746
1771
  const helper = this.helperEntry(task.data, true, true);
1747
1772
  if (!helper.ok) return helper;
1773
+ const ownFindings = task.data.findings
1774
+ .filter((candidate) => candidate.provenance.workerId === helperId)
1775
+ .map((candidate) => candidate.id);
1776
+ const reported: string[] = [];
1777
+ for (const findingRef of input.report.findingIds) {
1778
+ const found = resolveIdRef("finding", findingRef, ownFindings);
1779
+ if (found.ok) reported.push(found.data);
1780
+ else if (found.code === "not_found") reported.push(findingRef);
1781
+ else return found;
1782
+ }
1783
+ input.report.findingIds = reported;
1748
1784
  const evidence = task.data.evidence.filter((candidate) =>
1749
1785
  input.report.evidenceIds.includes(candidate.id),
1750
1786
  );
@@ -1800,8 +1836,7 @@ export class WorkitCore {
1800
1836
  : failure("recovery_required", "worker disappeared during report");
1801
1837
  }
1802
1838
 
1803
- if (helperId !== null)
1804
- return failure("permission_denied", "helpers cannot cancel or control workers");
1839
+ if (helperId !== null) return failure("permission_denied", NO_HELPER_CONTROL);
1805
1840
  if (task.data.status === "closed")
1806
1841
  return failure("invalid_transition", "closed task cannot cancel workers");
1807
1842
  if (entry.data.state === "stopped") {
@@ -47,6 +47,8 @@ import {
47
47
  success,
48
48
  taskRecordSchema,
49
49
  workspaceRecordSchema,
50
+ idRefPattern,
51
+ resolveIdRef,
50
52
  type Id,
51
53
  type Intent,
52
54
  type Provenance,
@@ -728,20 +730,23 @@ export class TaskStore {
728
730
  * the branch checked out now (#264). Null when no open task has it.
729
731
  */
730
732
  taskForWorker(workerId: unknown): Result<Id | null> {
731
- if (typeof workerId !== "string" || !validId(workerId))
733
+ if (typeof workerId !== "string" || !idRefPattern.test(workerId))
732
734
  return failure(
733
735
  "invalid_input",
734
736
  "needs a worker id: `workit worker cancel|report <workerId>`",
735
737
  );
736
738
  const listed = this.listTaskIndex();
737
739
  if (!listed.ok) return listed;
740
+ const owners = new Map<string, Id>();
738
741
  for (const entry of listed.data) {
739
742
  if (entry.status === "closed") continue;
740
743
  const loaded = this.load(entry.id);
741
744
  if (!loaded.ok) return loaded;
742
- if (loaded.data?.record.workers.some((worker) => worker.id === workerId))
743
- return success(null, null, entry.id);
745
+ for (const worker of loaded.data?.record.workers ?? []) owners.set(worker.id, entry.id);
744
746
  }
747
+ const resolved = resolveIdRef("worker", workerId, [...owners.keys()]);
748
+ if (resolved.ok) return success(null, null, owners.get(resolved.data) ?? null);
749
+ if (resolved.code !== "not_found") return resolved;
745
750
  return success(null, null, null);
746
751
  }
747
752