@brainervirus/workit-core 2.1.4 → 2.2.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.
Files changed (47) hide show
  1. package/package.json +3 -2
  2. package/scripts/analyze-release-scope.ts +3 -3
  3. package/scripts/doctor-check.ts +1 -2
  4. package/scripts/sync-release-manifests.ts +4 -4
  5. package/src/core/authority.ts +8 -8
  6. package/src/core/auto-approval.ts +6 -6
  7. package/src/core/branch.ts +4 -4
  8. package/src/core/config-conversion.ts +1 -1
  9. package/src/core/config.ts +3 -3
  10. package/src/core/cutover.ts +6 -6
  11. package/src/core/doctor.ts +49 -3
  12. package/src/core/external-action-effects.ts +15 -15
  13. package/src/core/external-action.ts +16 -19
  14. package/src/core/logger.ts +2 -2
  15. package/src/core/methods.ts +5 -0
  16. package/src/core/policy-resolver.ts +9 -6
  17. package/src/core/registration.ts +3 -5
  18. package/src/core/repo-context.ts +1 -1
  19. package/src/core/setup.ts +2 -2
  20. package/src/core/skill-manifests.ts +1 -1
  21. package/src/core/store-lock.ts +301 -0
  22. package/src/core/support-matrix.ts +1 -1
  23. package/src/core/task-context.ts +3 -3
  24. package/src/core/task-contract.ts +129 -9
  25. package/src/core/task-engine.ts +159 -159
  26. package/src/core/task-evaluation.ts +3 -3
  27. package/src/core/task-store.ts +407 -89
  28. package/src/core/tracker-issues.ts +1 -1
  29. package/src/core/uninstall.ts +3 -3
  30. package/src/core/workers.ts +3 -3
  31. package/src/core/workspaces.ts +4 -6
  32. package/src/core/youtrack-tools.ts +1 -1
  33. package/src/core.ts +1 -1
  34. package/src/git/rev.ts +736 -0
  35. package/src/{core/session-context.ts → hooks/context.ts} +97 -10
  36. package/src/hooks/descriptor.ts +113 -0
  37. package/src/hooks/handle.ts +111 -0
  38. package/src/hooks/hosts/claude-code.ts +281 -0
  39. package/src/hooks/hosts/codex.ts +298 -0
  40. package/src/hooks/hosts/cursor.ts +321 -0
  41. package/src/hooks/hosts/fields.ts +55 -0
  42. package/src/hooks/hosts/opencode.ts +79 -0
  43. package/src/hooks/hosts/pi.ts +80 -0
  44. package/src/hooks/index.ts +43 -0
  45. package/src/hooks/policy.ts +14 -0
  46. package/src/hooks/protocol.ts +89 -0
  47. package/src/hooks/run.ts +52 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "2.1.4",
3
+ "version": "2.2.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": [
@@ -36,6 +36,7 @@
36
36
  "type": "module",
37
37
  "main": "./src/core.ts",
38
38
  "exports": {
39
+ "./hooks": "./src/hooks/index.ts",
39
40
  "./src/*.ts": "./src/*.ts",
40
41
  "./src/*": "./src/*.ts",
41
42
  "./package.json": "./package.json"
@@ -48,7 +49,7 @@
48
49
  },
49
50
  "dependencies": {
50
51
  "@openclaw/fs-safe": "0.8.1",
51
- "zod": "4.5.4"
52
+ "zod": "4.6.5"
52
53
  },
53
54
  "engines": {
54
55
  "node": ">=24"
@@ -26,11 +26,11 @@ const g = (root: string, args: string[]): string =>
26
26
  const SEMVER_TAG = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
27
27
 
28
28
  export function latestTag(root = process.cwd()): string | null {
29
- const out = g(root, ["tag", "--list", "v*", "--sort=-v:refname"])
29
+ const tag = g(root, ["tag", "--list", "v*", "--sort=-v:refname"])
30
30
  .split("\n")
31
31
  .map((l) => l.trim())
32
- .filter((l) => SEMVER_TAG.test(l));
33
- return out[0] ?? null;
32
+ .find((l) => SEMVER_TAG.test(l));
33
+ return tag ?? null;
34
34
  }
35
35
 
36
36
  type Level = "major" | "minor" | "patch";
@@ -11,8 +11,7 @@
11
11
  import { runDoctor } from "../src/core/doctor";
12
12
 
13
13
  const hostArg = process.argv[2];
14
- const host =
15
- hostArg === "cursor" ? "cursor" : hostArg === "cli" ? "cli" : "opencode";
14
+ const host = hostArg === "cursor" ? "cursor" : hostArg === "cli" ? "cli" : "opencode";
16
15
  const staleOnly = process.argv.includes("--stale");
17
16
  const report = runDoctor({ host, installer: true });
18
17
 
@@ -31,7 +31,8 @@ export type ManifestSyncResult = { version: string; changed: string[] };
31
31
  * are left byte-untouched and omitted from `changed`. Throws on a value that
32
32
  * is not a plain release version (`latest`, branches, ranges).
33
33
  */
34
- export function syncManifests(root: string, tagOrVersion: string): ManifestSyncResult { const match = /^v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/.exec(tagOrVersion.trim());
34
+ export function syncManifests(root: string, tagOrVersion: string): ManifestSyncResult {
35
+ const match = /^v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/.exec(tagOrVersion.trim());
35
36
  if (!match) throw new Error(`invalid version tag: ${JSON.stringify(tagOrVersion)}`);
36
37
  const version = match[1];
37
38
  const changed: string[] = [];
@@ -48,15 +49,14 @@ export function syncManifests(root: string, tagOrVersion: string): ManifestSyncR
48
49
 
49
50
  /** The newest `v*` tag by descending semver refname order (empty repo throws). */
50
51
  function latestReleaseTag(cwd?: string): string {
51
- const out = execFileSync("git", ["tag", "--list", "v*", "--sort=-v:refname"], {
52
+ const tag = execFileSync("git", ["tag", "--list", "v*", "--sort=-v:refname"], {
52
53
  cwd,
53
54
  encoding: "utf8",
54
55
  stdio: ["pipe", "pipe", "pipe"],
55
56
  })
56
57
  .split("\n")
57
58
  .map((line) => line.trim())
58
- .filter(Boolean);
59
- const tag = out[0];
59
+ .find(Boolean);
60
60
  if (!tag) throw new Error("no v* release tag found — run after the first semantic-release");
61
61
  return tag;
62
62
  }
@@ -641,7 +641,7 @@ const validateAction = (
641
641
  if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
642
642
  return failure("permission_denied", "action binding does not match the approved decision");
643
643
  const content = verifyContentRefs(store, decision.binding);
644
- if (!content.ok) return content as Result<never>;
644
+ if (!content.ok) return content;
645
645
  const requestedSteps = parseSteps(decision.binding.approvedContent);
646
646
  if (requestedSteps === null)
647
647
  return failure("invalid_input", "bounded action steps must be a unique non-empty array");
@@ -679,7 +679,7 @@ export function reserveAction(input: ReserveActionInput): Result<ActionReservati
679
679
  input.expectedRevision,
680
680
  (current, mutation) => {
681
681
  const workspace = input.store.readWorkspace();
682
- if (!workspace.ok) return workspace as Result<never>;
682
+ if (!workspace.ok) return workspace;
683
683
  if (!workspace.data) return failure("not_found", "workspace not found");
684
684
  if (workspace.data.revision !== input.expectedWorkspaceRevision)
685
685
  return failure("revision_conflict", "workspace revision does not match", {
@@ -687,7 +687,7 @@ export function reserveAction(input: ReserveActionInput): Result<ActionReservati
687
687
  actualWorkspaceRevision: workspace.data.revision,
688
688
  });
689
689
  const valid = validateAction(input.store, current, workspace.data.id, input, authority);
690
- if (!valid.ok) return valid as Result<never>;
690
+ if (!valid.ok) return valid;
691
691
  const { entry, workflow } = valid.data;
692
692
  const existing = entry.data.consumption;
693
693
  if (existing?.state === "uncertain")
@@ -736,7 +736,7 @@ export function reserveAction(input: ReserveActionInput): Result<ActionReservati
736
736
  },
737
737
  input.native?.now,
738
738
  );
739
- if (!changed.ok) return changed as Result<never>;
739
+ if (!changed.ok) return changed;
740
740
  const workspace = input.store.readWorkspace();
741
741
  if (!workspace.ok || !workspace.data)
742
742
  return failure("recovery_required", "workspace disappeared after reservation");
@@ -793,7 +793,7 @@ export function settleAction(input: SettleActionInput): Result<Entry<Decision>>
793
793
  input.taskRevision,
794
794
  (current, mutation) => {
795
795
  const workspace = input.store.readWorkspace();
796
- if (!workspace.ok) return workspace as Result<never>;
796
+ if (!workspace.ok) return workspace;
797
797
  if (!workspace.data) return failure("not_found", "workspace not found");
798
798
  if (
799
799
  !authorityMatches(authority, {
@@ -909,7 +909,7 @@ export function settleAction(input: SettleActionInput): Result<Entry<Decision>>
909
909
  },
910
910
  input.native?.now,
911
911
  );
912
- if (!changed.ok) return changed as Result<never>;
912
+ if (!changed.ok) return changed;
913
913
  const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
914
914
  if (!entry) return failure("recovery_required", "settled decision disappeared");
915
915
  if (outcomeResult) return outcomeResult;
@@ -943,7 +943,7 @@ export function reconcileAction(input: ReconcileActionInput): Result<Entry<Decis
943
943
  input.taskRevision,
944
944
  (current, mutation) => {
945
945
  const workspace = input.store.readWorkspace();
946
- if (!workspace.ok) return workspace as Result<never>;
946
+ if (!workspace.ok) return workspace;
947
947
  if (!workspace.data) return failure("not_found", "workspace not found");
948
948
  if (authority.workspaceId !== workspace.data.id)
949
949
  return failure("permission_denied", "native reconciliation workspace does not match");
@@ -1022,7 +1022,7 @@ export function reconcileAction(input: ReconcileActionInput): Result<Entry<Decis
1022
1022
  },
1023
1023
  input.native?.now,
1024
1024
  );
1025
- if (!changed.ok) return changed as Result<never>;
1025
+ if (!changed.ok) return changed;
1026
1026
  const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
1027
1027
  return entry
1028
1028
  ? success(changed.data.revision, input.workspaceRevision, entry)
@@ -172,7 +172,7 @@ export const verifyStandingApproval = (
172
172
  if (task.status !== "active")
173
173
  return failure("invalid_transition", "only active tasks can authorize actions");
174
174
  const workspace = new TaskStore(root).readWorkspace();
175
- if (!workspace.ok) return workspace as Result<never>;
175
+ if (!workspace.ok) return workspace;
176
176
  if (!workspace.data || !currentWriterOwnsTask(task, workspace.data, caller))
177
177
  return failure("permission_denied", "standing approval requires the current writer session");
178
178
  const standing = binding.standing;
@@ -200,8 +200,8 @@ export const verifyStandingApproval = (
200
200
  )
201
201
  return failure("permission_denied", "standing auto-approval is not live for this operation");
202
202
  if (cls === "push") {
203
- const workspace = resolveWorkspaceFrom(target, configDir());
204
- const provider = workspace?.vcs?.provider;
203
+ const targetWorkspace = resolveWorkspaceFrom(target, configDir());
204
+ const provider = targetWorkspace?.vcs?.provider;
205
205
  const account = resolved.account;
206
206
  if (!provider || !account)
207
207
  return failure(
@@ -217,7 +217,7 @@ export const verifyStandingApproval = (
217
217
  session: { kind: "host", host: caller.host, handle: caller.actor },
218
218
  workerId: null,
219
219
  receipts: [standingReceiptFor(target, standing.workspace, cls)],
220
- } as Provenance);
220
+ });
221
221
  };
222
222
 
223
223
  export type StandingAutoBinding = {
@@ -259,8 +259,8 @@ export const standingAutoApplies = (
259
259
  const listed = store.listTasks();
260
260
  const workspace = store.readWorkspace();
261
261
  if (!listed.ok || !workspace.ok || !workspace.data) return null;
262
- const task = listed.data.find((task) =>
263
- currentWriterOwnsTask(task, workspace.data!, { host, actor }),
262
+ const task = listed.data.find((entry) =>
263
+ currentWriterOwnsTask(entry, workspace.data!, { host, actor }),
264
264
  );
265
265
  if (!task) return null;
266
266
  const resolved = resolveAutoApproval(target);
@@ -132,8 +132,8 @@ export const branchPolicySnapshotFor = (workspaceRoot: string) => {
132
132
  const source = resolved.provenance.branchPolicy;
133
133
  const allowed = branchPolicy.allowed
134
134
  .map((pattern) => `${pattern.source}/${pattern.flags}`)
135
- .sort();
136
- const protectedRefs = [...branchPolicy.protected].map((name) => name.toLowerCase()).sort();
135
+ .toSorted();
136
+ const protectedRefs = [...branchPolicy.protected].map((name) => name.toLowerCase()).toSorted();
137
137
  const fingerprint = createHash("sha256")
138
138
  .update(JSON.stringify({ allowed, protectedRefs, source }))
139
139
  .digest("hex");
@@ -675,8 +675,8 @@ export const branchSetup = ({
675
675
  targetExists = false;
676
676
  }
677
677
  if (!targetExists) {
678
- const policy = validateBranchNameFor(cwd, target);
679
- if (!policy.ok) return { error: policy.error, phase: "preflight" };
678
+ const namePolicy = validateBranchNameFor(cwd, target);
679
+ if (!namePolicy.ok) return { error: namePolicy.error, phase: "preflight" };
680
680
  }
681
681
  if (!targetExists) {
682
682
  const baseResolved = baseBranch(cwd);
@@ -70,7 +70,7 @@ export function previewConversion(input: ConversionInput): ConversionPreview {
70
70
  allowed?: string[];
71
71
  protected?: string[];
72
72
  };
73
- const preset = (bp.preset ?? "gitflow") as BranchPreset;
73
+ const preset = bp.preset ?? "gitflow";
74
74
  const merged = mergePreset(
75
75
  preset,
76
76
  {
@@ -297,7 +297,7 @@ export const resolveBranchPolicy = (
297
297
  integration: "pr" | "merge";
298
298
  defaultTargetBranch: string;
299
299
  } => {
300
- const wp = (workspace?.branchPolicy ?? {}) as Record<string, any>;
300
+ const wp = workspace?.branchPolicy ?? {};
301
301
  // An invalid workspace preset (e.g. a typo) falls back to the global preset,
302
302
  // preserving resolution order workspace > global > preset, instead of
303
303
  // crashing on PRESETS[preset] (mirrors parseConfigResult's Object.hasOwn).
@@ -345,12 +345,12 @@ export const resolveCommitPolicy = (
345
345
  config: ToolkitConfig,
346
346
  workspace?: { commitPolicy?: Record<string, any> } | null,
347
347
  ): { preset: CommitFlavorPreset; pattern?: string } => {
348
- const wp = (workspace?.commitPolicy ?? {}) as Record<string, any>;
348
+ const wp = workspace?.commitPolicy ?? {};
349
349
  if (COMMIT_PRESETS.includes(String(wp.preset ?? ""))) {
350
350
  return {
351
351
  preset: wp.preset as CommitFlavorPreset,
352
352
  ...(typeof wp.pattern === "string" ? { pattern: wp.pattern } : {}),
353
353
  };
354
354
  }
355
- return config.commitPolicy ?? ({ preset: "conventional" } as ToolkitConfig["commitPolicy"]);
355
+ return config.commitPolicy ?? { preset: "conventional" };
356
356
  };
@@ -217,8 +217,8 @@ const readDirNamesBounded = (
217
217
  try {
218
218
  for (;;) {
219
219
  const entry = handle.readSync();
220
- if (!entry) return { names: names.sort(), truncated: false };
221
- if (names.length === limit) return { names: names.sort(), truncated: true };
220
+ if (!entry) return { names: names.toSorted(), truncated: false };
221
+ if (names.length === limit) return { names: names.toSorted(), truncated: true };
222
222
  names.push(entry.name);
223
223
  }
224
224
  } finally {
@@ -596,8 +596,8 @@ const cutoverInventoryScopes = (
596
596
  category: "project-state",
597
597
  disposition: "preserve",
598
598
  },
599
- ...legacyFlowInventoryTargets(paths.workspace, issues).map((path) => ({
600
- path,
599
+ ...legacyFlowInventoryTargets(paths.workspace, issues).map((target) => ({
600
+ path: target,
601
601
  category: "project-history" as const,
602
602
  disposition: "preserve" as const,
603
603
  })),
@@ -743,7 +743,7 @@ const buildCutoverInventory = (paths: ReturnType<typeof resolvePaths>): CutoverI
743
743
  }
744
744
  hash.update(`${entry.path}\0${entry.kind}\0${entry.bytes}\0${entry.digest ?? ""}\n`);
745
745
  }
746
- const issueList = [...issues].sort();
746
+ const issueList = [...issues].toSorted();
747
747
  for (const issue of issueList) hash.update(`!${issue}\n`);
748
748
  return {
749
749
  complete: issueList.length === 0,
@@ -936,7 +936,7 @@ const readBackupFiles = (
936
936
  issues.push(`backup content failed verification: ${entry.path}`);
937
937
  continue;
938
938
  }
939
- files.set(entry.path, { digest: digest as Digest, storagePath });
939
+ files.set(entry.path, { digest: digest, storagePath });
940
940
  }
941
941
  return { files, issues };
942
942
  };
@@ -19,6 +19,7 @@ import {
19
19
  import os from "node:os";
20
20
  import path from "node:path";
21
21
  import { SUPPORT_MATRIX } from "./support-matrix";
22
+ import { inspectMetadataLock } from "./store-lock";
22
23
  import { bundleHashOfFile, isEphemeralCachePath } from "./runtime-identity";
23
24
  import { EVENT } from "./boundary";
24
25
  import { getDiagnosticLogger, isConfigObject } from "./config";
@@ -61,6 +62,7 @@ export type DoctorCheckId =
61
62
  | "duplicate_registration"
62
63
  | "malformed_config"
63
64
  | "workspace_mismatch"
65
+ | "workspace_lock"
64
66
  | "credential_metadata"
65
67
  | "github_identity"
66
68
  | "gitlab_identity"
@@ -110,6 +112,8 @@ export type DoctorOptions = {
110
112
  /** Checkout containing packages/ (monorepo or share clone). */
111
113
  dev?: string;
112
114
  cwd?: string;
115
+ /** Workit store root for the lock check (default: WORKFLOW_WORKSPACE_ROOT, then cwd). */
116
+ workspaceRoot?: string;
113
117
  opencodeConfig?: string;
114
118
  /** OpenCode npm `@latest` package cache root (test seam). */
115
119
  opencodePackageCacheDir?: string;
@@ -129,6 +133,7 @@ type Resolved = {
129
133
  configDir: string;
130
134
  stateDir: string;
131
135
  cwd: string;
136
+ workspaceRoot: string;
132
137
  dev: string | null;
133
138
  opencodeConfig: string;
134
139
  opencodePackageCacheDir: string;
@@ -179,6 +184,7 @@ const resolve = (options: DoctorOptions): Resolved => {
179
184
  configDir,
180
185
  stateDir,
181
186
  cwd,
187
+ workspaceRoot: options.workspaceRoot ?? env.WORKFLOW_WORKSPACE_ROOT ?? cwd,
182
188
  dev,
183
189
  opencodeConfig:
184
190
  options.opencodeConfig ?? path.join(home, ".config", "opencode", "opencode.json"),
@@ -559,7 +565,8 @@ const registeredCursorLauncher = (res: Resolved): CursorLauncher | null | "inval
559
565
  // no version abstraction.
560
566
  const canonicalCursorHook = cursorHooksEntry("").command;
561
567
 
562
- const registeredCursorHook = (res: Resolved): string | null | "invalid" => {
568
+ // Returns the registered hook command, null when absent, or "invalid".
569
+ const registeredCursorHook = (res: Resolved): string | null => {
563
570
  const hooksFile = path.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
564
571
  if (!existsSync(hooksFile)) return "invalid";
565
572
  const config = readJson(hooksFile);
@@ -866,7 +873,7 @@ const checkOpenCodePackageCache = (res: Resolved): DoctorCheck & { registryProbe
866
873
  detail: "no workit opencode pin — package cache not inspected",
867
874
  };
868
875
  }
869
- const pin = entries[0]!;
876
+ const pin = entries[0];
870
877
  if (pin.startsWith("file:") || pin.startsWith("git+file:")) {
871
878
  return {
872
879
  id: "stale_install",
@@ -1318,7 +1325,7 @@ const providerIdentityFinding = (
1318
1325
  provider: "github" | "gitlab",
1319
1326
  surfaces: IdentitySurface[],
1320
1327
  ): DoctorCheck => {
1321
- const id = `${provider}_identity` as "github_identity" | "gitlab_identity";
1328
+ const id: "github_identity" | "gitlab_identity" = `${provider}_identity`;
1322
1329
  const label = provider === "github" ? "GitHub" : "GitLab";
1323
1330
  const resolved = surfaces.filter(
1324
1331
  (entry): entry is { surface: string; login: string } => typeof entry.login === "string",
@@ -1692,6 +1699,44 @@ const checkManagedContentConflict = (res: Resolved): DoctorCheck => {
1692
1699
  };
1693
1700
  };
1694
1701
 
1702
+ // The checkout's `.workit/metadata.lock`. Writes reclaim a stale lock by
1703
+ // themselves, so a stale lock is a warning with an explicit cleanup command.
1704
+ const BLOCKING_LOCK_WARN_MS = 30_000;
1705
+ const checkWorkspaceLock = (res: Resolved): DoctorCheck => {
1706
+ const lock = inspectMetadataLock(res.workspaceRoot);
1707
+ const fix = "workit doctor --fix-lock";
1708
+ if (lock.guard === "abandoned")
1709
+ return {
1710
+ id: "workspace_lock",
1711
+ status: "warn",
1712
+ detail: `abandoned lock reclaim guard at ${lock.path}.reclaim`,
1713
+ fix,
1714
+ };
1715
+ if (lock.state === "absent")
1716
+ return { id: "workspace_lock", status: "pass", detail: "no metadata lock held" };
1717
+ if (lock.state === "stale")
1718
+ return {
1719
+ id: "workspace_lock",
1720
+ status: "warn",
1721
+ detail: `stale metadata lock at ${lock.path}: ${lock.reason}`,
1722
+ fix,
1723
+ };
1724
+ // An unverifiable owner (other host, pid namespace, or an older Workit's
1725
+ // lock) that has blocked writes this long needs an explicit decision.
1726
+ if (lock.state === "unknown" && (lock.ageMs ?? 0) > BLOCKING_LOCK_WARN_MS)
1727
+ return {
1728
+ id: "workspace_lock",
1729
+ status: "warn",
1730
+ detail: `metadata lock at ${lock.path} has blocked writes for ${Math.round((lock.ageMs ?? 0) / 1000)}s and its owner cannot be verified: ${lock.reason}`,
1731
+ fix: "workit doctor --fix-lock --force --yes",
1732
+ };
1733
+ return {
1734
+ id: "workspace_lock",
1735
+ status: "pass",
1736
+ detail: `metadata lock ${lock.reason} (writes retry, then report busy)`,
1737
+ };
1738
+ };
1739
+
1695
1740
  const RUN_CHECKS: Array<(res: Resolved) => DoctorCheck> = [
1696
1741
  checkRuntime,
1697
1742
  checkVersions,
@@ -1704,6 +1749,7 @@ const RUN_CHECKS: Array<(res: Resolved) => DoctorCheck> = [
1704
1749
  checkDuplicateRegistration,
1705
1750
  checkMalformedConfig,
1706
1751
  checkWorkspaceMismatch,
1752
+ checkWorkspaceLock,
1707
1753
  checkCredentialMetadata,
1708
1754
  checkGithubIdentity,
1709
1755
  checkGitLabIdentity,
@@ -86,9 +86,9 @@ const stagedPaths = (root: string): string[] | null => {
86
86
  const rename = /^(?:R|C)\d{3}$/.test(status);
87
87
  const count = rename ? 2 : 1;
88
88
  for (let offset = 0; offset < count; offset += 1) {
89
- const path = tokens[index++];
90
- if (!path) return null;
91
- paths.push(path);
89
+ const filePath = tokens[index++];
90
+ if (!filePath) return null;
91
+ paths.push(filePath);
92
92
  }
93
93
  }
94
94
  return [...new Set(paths)];
@@ -258,8 +258,8 @@ export const assertLocalExternalActionWriter = (
258
258
  const store = new TaskStore(root);
259
259
  const listed = store.listTasks();
260
260
  const workspace = store.readWorkspace();
261
- if (!listed.ok) return listed as Result<never>;
262
- if (!workspace.ok) return workspace as Result<never>;
261
+ if (!listed.ok) return listed;
262
+ if (!workspace.ok) return workspace;
263
263
  if (!workspace.data) return failure("not_found", "workspace not found");
264
264
  if (
265
265
  expectedWorkspaceRevision !== undefined &&
@@ -317,7 +317,7 @@ const branchCreationBaseline = (
317
317
  };
318
318
  const remote = run(root, ["ls-remote", "origin", `refs/heads/${base}`]);
319
319
  const sha = remote.exitCode === 0 ? remote.stdout.trim().split(/\s+/)[0] : "";
320
- const remoteBase = /^[0-9a-f]{40}$/.test(sha ?? "") ? (sha as string) : null;
320
+ const remoteBase = /^[0-9a-f]{40}$/.test(sha ?? "") ? sha : null;
321
321
  return {
322
322
  base_branch: base,
323
323
  target_exists: false,
@@ -717,7 +717,7 @@ const mergedBranch = (root: string, branch: string): Result<Record<string, strin
717
717
  },
718
718
  );
719
719
  const live = remoteBranchTip(root, provider, auth.host, repo, branch);
720
- if (!live.ok) return live as Result<never>;
720
+ if (!live.ok) return live;
721
721
  const tip = live.data;
722
722
  if (!tip)
723
723
  return failure("invalid_input", "remote branch is absent or could not be read", {
@@ -978,8 +978,8 @@ export const readYouTrackAction = async (
978
978
  const creds = youTrackToken();
979
979
  if ("error" in creds || creds.base !== baseUrl) return unknownHostingEvidence(operation);
980
980
  const bounded = "&$top=100&$skip=0";
981
- const read = async (path: string): Promise<unknown[] | null> => {
982
- const result = await youTrackRequest(`${baseUrl}${path}${bounded}`, {
981
+ const read = async (endpoint: string): Promise<unknown[] | null> => {
982
+ const result = await youTrackRequest(`${baseUrl}${endpoint}${bounded}`, {
983
983
  method: "GET",
984
984
  token: creds.token,
985
985
  });
@@ -1670,7 +1670,7 @@ const resolveExternalActionAtRoot = (
1670
1670
  request,
1671
1671
  descriptorPayload: { ...request.payload, resolved: checked.data },
1672
1672
  })
1673
- : (checked as Result<never>);
1673
+ : checked;
1674
1674
  }
1675
1675
  case "changelog.apply": {
1676
1676
  const preview = changelogApplyPreview({ ...request.payload, workspace_root: root });
@@ -1707,7 +1707,7 @@ const resolveExternalActionAtRoot = (
1707
1707
  return failure("invalid_input", "youtrack.time requires an explicit dateMs");
1708
1708
  {
1709
1709
  const available = youTrackWritePreflight();
1710
- if (!available.ok) return available as Result<ResolvedExternalAction>;
1710
+ if (!available.ok) return available;
1711
1711
  const baseUrl = youTrackBase();
1712
1712
  const date = youTrackWorkDateMs(String(request.payload.dateMs));
1713
1713
  if (!baseUrl)
@@ -1741,7 +1741,7 @@ const resolveExternalActionAtRoot = (
1741
1741
  case "youtrack.update":
1742
1742
  case "youtrack.meeting": {
1743
1743
  const available = youTrackWritePreflight();
1744
- if (!available.ok) return available as Result<ResolvedExternalAction>;
1744
+ if (!available.ok) return available;
1745
1745
  const baseUrl = youTrackBase();
1746
1746
  const date = youTrackWorkDateMs(
1747
1747
  preservedDateMs === undefined ? "auto" : String(preservedDateMs),
@@ -1787,7 +1787,7 @@ const resolveExternalActionAtRoot = (
1787
1787
  }
1788
1788
  default:
1789
1789
  return success(null, null, {
1790
- request: request as never,
1790
+ request: request,
1791
1791
  descriptorPayload: (request as unknown as { payload: unknown }).payload,
1792
1792
  });
1793
1793
  }
@@ -1802,7 +1802,7 @@ export const resolveExternalActionRequest = (
1802
1802
  preservedDateMs?: number,
1803
1803
  ): Result<ResolvedExternalAction> => {
1804
1804
  const target = actionRoot(taskRoot, request);
1805
- if (!target.ok) return target as Result<never>;
1805
+ if (!target.ok) return target;
1806
1806
  const bindTarget = bindsActionTarget(request.operation);
1807
1807
  const normalized = bindTarget
1808
1808
  ? ({ ...request, payload: { ...request.payload, cwd: target.data } } as ExternalActionRequest)
@@ -1909,7 +1909,7 @@ export const executeConcreteExternalAction = async (
1909
1909
  );
1910
1910
  if (!sameDirectoryIdentity(root, coordinationRoot)) {
1911
1911
  const targetWorkspace = new TaskStore(root).readWorkspace();
1912
- if (!targetWorkspace.ok) return targetWorkspace as Result<never>;
1912
+ if (!targetWorkspace.ok) return targetWorkspace;
1913
1913
  const owner = targetWorkspace.data?.writer?.owner;
1914
1914
  if (
1915
1915
  owner &&
@@ -210,11 +210,10 @@ export const externalActionSchema = z.discriminatedUnion("operation", [
210
210
  export type ExternalActionRequest = z.infer<typeof externalActionSchema>;
211
211
 
212
212
  /** The complete external-action contract for hosts that register native tools. */
213
- export const externalActionJsonSchema = (): z.core.JSONSchema.BaseSchema =>
214
- ({
215
- type: "object",
216
- ...z.toJSONSchema(externalActionSchema, { target: "draft-2020-12" }),
217
- }) as z.core.JSONSchema.BaseSchema;
213
+ export const externalActionJsonSchema = (): z.core.JSONSchema.BaseSchema => ({
214
+ type: "object",
215
+ ...z.toJSONSchema(externalActionSchema, { target: "draft-2020-12" }),
216
+ });
218
217
 
219
218
  export type ExternalActionOperation = ExternalActionRequest["operation"];
220
219
 
@@ -639,7 +638,7 @@ export const chainStepBinding = (
639
638
  typeof descriptor.payload?.target_branch === "string"
640
639
  ? {
641
640
  operation: "git.branch_setup",
642
- target: descriptor.payload.target_branch as string,
641
+ target: descriptor.payload.target_branch,
643
642
  cwd: targetRoot,
644
643
  }
645
644
  : descriptor.operation === "hosting.pull_request"
@@ -947,7 +946,7 @@ export const createAuthorizedExternalActionRunner =
947
946
  let latest: ExternalActionResult<T> | undefined;
948
947
  for (const step of sequence) {
949
948
  const binding = bind(operation);
950
- if ("ok" in binding) return binding as ExternalActionResult<T>;
949
+ if ("ok" in binding) return binding;
951
950
  const targetRoot = externalActionTargetRoot(operation, coordinationRoot);
952
951
  if (targetRoot === null)
953
952
  return failure("capability_unavailable", "external action target could not be locked", {
@@ -960,7 +959,7 @@ export const createAuthorizedExternalActionRunner =
960
959
  ? (run) => new TaskStore(targetRoot).withExternalActionLock(run, true)
961
960
  : undefined;
962
961
  const result = await runAuthorizedExternalAction(
963
- { core, ...(binding as Omit<AuthorizedActionInput, "core">), ...(step ? { step } : {}) },
962
+ { core, ...binding, ...(step ? { step } : {}) },
964
963
  (reservation) => effect(step, reservation),
965
964
  withActionLock,
966
965
  );
@@ -1039,12 +1038,10 @@ export const readNativeExternalActionObservation = (
1039
1038
  actionRef: candidate.actionRef as Ref,
1040
1039
  outcome: candidate.outcome as NativeExternalActionObservation["outcome"],
1041
1040
  ...(candidate.callId === undefined ? {} : { callId: candidate.callId }),
1042
- ...(candidate.taskRevision === undefined
1043
- ? {}
1044
- : { taskRevision: candidate.taskRevision as Revision }),
1041
+ ...(candidate.taskRevision === undefined ? {} : { taskRevision: candidate.taskRevision }),
1045
1042
  ...(candidate.workspaceRevision === undefined
1046
1043
  ? {}
1047
- : { workspaceRevision: candidate.workspaceRevision as Revision }),
1044
+ : { workspaceRevision: candidate.workspaceRevision }),
1048
1045
  };
1049
1046
  };
1050
1047
 
@@ -1100,7 +1097,7 @@ export async function runAuthorizedExternalAction<T>(
1100
1097
  ...(input.step ? { step: input.step } : {}),
1101
1098
  observation: input.reserveObservation,
1102
1099
  });
1103
- if (!reserved.ok) return reserved as ExternalActionResult<T>;
1100
+ if (!reserved.ok) return reserved;
1104
1101
 
1105
1102
  const settle = (
1106
1103
  outcome: "succeeded" | "not_started" | "unknown",
@@ -1149,7 +1146,7 @@ export async function runAuthorizedExternalAction<T>(
1149
1146
  effectValue = await effect(reserved.data);
1150
1147
  } catch {
1151
1148
  const settled = settleWithRefresh("unknown", settle("unknown"));
1152
- if (!settled.ok) return settled as ExternalActionResult<T>;
1149
+ if (!settled.ok) return settled;
1153
1150
  return failure("external_outcome_unknown", "external action outcome is unknown", {
1154
1151
  operation: "external_action",
1155
1152
  outcome: "unknown",
@@ -1161,7 +1158,7 @@ export async function runAuthorizedExternalAction<T>(
1161
1158
  "ok" in effectValue &&
1162
1159
  typeof (effectValue as { ok?: unknown }).ok === "boolean"
1163
1160
  ) {
1164
- const contract = effectValue as Result<T>;
1161
+ const contract = effectValue;
1165
1162
  if (!contract.ok) {
1166
1163
  const preflight =
1167
1164
  typeof contract.details === "object" &&
@@ -1169,13 +1166,13 @@ export async function runAuthorizedExternalAction<T>(
1169
1166
  (contract.details as Record<string, unknown>).outcome === "not_started";
1170
1167
  const outcome = input.failureOutcome ?? (preflight ? "not_started" : "unknown");
1171
1168
  const settled = settleWithRefresh(outcome, settle(outcome));
1172
- if (!settled.ok) return settled as ExternalActionResult<T>;
1169
+ if (!settled.ok) return settled;
1173
1170
  return contract;
1174
1171
  }
1175
1172
  effectValue = contract.data;
1176
1173
  }
1177
1174
  const settled = settleWithRefresh("succeeded", settle("succeeded"));
1178
- if (!settled.ok) return settled as ExternalActionResult<T>;
1175
+ if (!settled.ok) return settled;
1179
1176
  return {
1180
1177
  ok: true,
1181
1178
  schemaVersion: 1,
@@ -1194,14 +1191,14 @@ export async function runAuthorizedExternalAction<T>(
1194
1191
  outcome: "unknown",
1195
1192
  });
1196
1193
  const settled = settleWithRefresh("not_started", settle("not_started"));
1197
- if (!settled.ok) return settled as ExternalActionResult<T>;
1194
+ if (!settled.ok) return settled;
1198
1195
  return failure("writer_conflict", "target checkout was unavailable before the effect", {
1199
1196
  outcome: "not_started",
1200
1197
  });
1201
1198
  }
1202
1199
  if (!effectStarted) {
1203
1200
  const settled = settleWithRefresh("not_started", settle("not_started"));
1204
- if (!settled.ok) return settled as ExternalActionResult<T>;
1201
+ if (!settled.ok) return settled;
1205
1202
  }
1206
1203
  return result;
1207
1204
  }
@@ -216,8 +216,8 @@ const pruneOldFiles = (dir: string): void => {
216
216
  try {
217
217
  const files = readdirSync(dir)
218
218
  .filter((name) => DAY_FILE.test(name))
219
- .sort()
220
- .reverse();
219
+ .toSorted()
220
+ .toReversed();
221
221
  for (const file of files.slice(RETAINED_DAYS)) {
222
222
  try {
223
223
  unlinkSync(path.join(dir, file));
@@ -110,6 +110,11 @@ Start a record once for an explicit tracked objective; assess or reassess only
110
110
  when policy selection or changed evidence/constraints requires it. Omitted
111
111
  expectedRevision and expectedWorkspaceRevision use current values; explicit
112
112
  values are still concurrency-checked, so never copy revisions between calls.
113
+ A busy result means another live Workit call holds the checkout lock: retry the
114
+ same call; it is not a recovery condition. A lock left by a dead process is
115
+ reclaimed on the next write, and \`workit doctor --fix-lock\` clears it on demand.
116
+ A revision_conflict on a call that omitted expectedRevision is contention too:
117
+ re-read the record and retry the call.
113
118
  A solo edit does not need writer acquisition; use it when concurrent checkout
114
119
  writers need coordination. Record only observed facts and checks. Evidence can
115
120
  become stale when its bound candidate changes; reconcile findings against the