@brainervirus/workit-codex 3.0.0 → 5.0.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/.codex-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/dist/launch-mcp.js +2383 -1259
- package/dist/workit-hook.js +2396 -1270
- package/package.json +3 -3
- package/skills/workit-bdd/SKILL.md +35 -29
- package/skills/workit-continue/SKILL.md +53 -0
- package/skills/workit-debug/SKILL.md +41 -36
- package/skills/workit-deslop/SKILL.md +35 -25
- package/skills/workit-fanout/SKILL.md +62 -0
- package/skills/workit-fanout/references/brief.md +56 -0
- package/skills/workit-implement/SKILL.md +48 -44
- package/skills/workit-review/SKILL.md +42 -51
- package/skills/workit-review/references/impact.md +24 -0
- package/skills/workit-shape/SKILL.md +71 -0
- package/skills/workit-shape/references/diagrams.md +17 -0
- package/skills/workit-shape/references/knowledge.md +58 -0
- package/skills/workit-shape/references/mockups.md +15 -0
- package/skills/workit-shape/references/slicing.md +42 -0
- package/skills/workit-ship/SKILL.md +52 -0
- package/skills/workit-test-audit/SKILL.md +10 -2
- package/skills/workit-verify-app/SKILL.md +63 -0
- package/skills/workit-verify-app/references/template.md +49 -0
- package/skills/workit-babysit/SKILL.md +0 -37
- package/skills/workit-behavioral-tdd/SKILL.md +0 -56
- package/skills/workit-blast-radius/SKILL.md +0 -26
- package/skills/workit-challenge/SKILL.md +0 -47
- package/skills/workit-diagram/SKILL.md +0 -27
- package/skills/workit-green-run/SKILL.md +0 -24
- package/skills/workit-handoff/SKILL.md +0 -37
- package/skills/workit-mockup/SKILL.md +0 -23
- package/skills/workit-plan/SKILL.md +0 -45
- package/skills/workit-steer/SKILL.md +0 -39
package/dist/workit-hook.js
CHANGED
|
@@ -26,17 +26,17 @@ var capabilitiesFor = (descriptor, observed = {}) => descriptor.capabilities.map
|
|
|
26
26
|
};
|
|
27
27
|
});
|
|
28
28
|
// packages/workit-core/src/hooks/context.ts
|
|
29
|
-
import
|
|
29
|
+
import path15 from "node:path";
|
|
30
30
|
|
|
31
31
|
// packages/workit-core/src/core/methods.ts
|
|
32
32
|
var METHODS = {
|
|
33
|
-
"workit-
|
|
34
|
-
"workit-
|
|
33
|
+
"workit-shape": { dimensions: ["challenge", "decisions", "artifacts", "continuity"] },
|
|
34
|
+
"workit-bdd": { dimensions: ["testing"] },
|
|
35
35
|
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
|
|
36
|
-
"workit-
|
|
37
|
-
"workit-
|
|
36
|
+
"workit-implement": { ruleIds: ["mechanical-existing-checks"] },
|
|
37
|
+
"workit-fanout": { dimensions: ["delegation"] },
|
|
38
38
|
"workit-debug": {},
|
|
39
|
-
"workit-
|
|
39
|
+
"workit-continue": {},
|
|
40
40
|
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
|
|
41
41
|
};
|
|
42
42
|
var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
|
|
@@ -74,94 +74,66 @@ function selectMethods(policy, capabilities) {
|
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
76
|
var invariantBootstrap = () => `
|
|
77
|
-
Workit is optional coordination and
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
decisions make continuity useful; similar titles alone never merge tasks.
|
|
84
|
-
|
|
85
|
-
For a routine user-authorized branch or commit, prefer native host Git/shell
|
|
86
|
-
tools from the outset when managed coordination or outcome reconciliation is
|
|
87
|
-
not needed. Inspect the actual target checkout and its configured conventions;
|
|
88
|
-
use Workit's read-only context/policy tools when needed. Do not start a task,
|
|
89
|
-
acquire a writer, or mint a decision receipt solely for a native Git action.
|
|
90
|
-
Native permissions still apply. Never switch execution paths to evade a denial
|
|
91
|
-
or repeat an uncertain managed effect. A local-commit endpoint does not imply
|
|
92
|
-
PR readiness, a fresh review, or post-commit assessment/closure paperwork.
|
|
93
|
-
|
|
94
|
-
For tracked work, inspect its current state and use the shared operations for
|
|
95
|
-
task, policy, evidence, finding, decision, worker, writer, and state changes.
|
|
96
|
-
Start a record once for an explicit tracked objective; assess or reassess only
|
|
97
|
-
when policy selection or changed evidence/constraints requires it. Omitted
|
|
98
|
-
expectedRevision and expectedWorkspaceRevision use current values; explicit
|
|
99
|
-
values are still concurrency-checked, so never copy revisions between calls.
|
|
100
|
-
A busy result means another live Workit call holds the checkout lock: retry the
|
|
101
|
-
same call; it is not a recovery condition. A lock left by a dead process is
|
|
102
|
-
reclaimed on the next write, and \`workit doctor --fix-lock\` clears it on demand.
|
|
103
|
-
An omitted revision absorbs a concurrent write: Workit re-reads, re-checks
|
|
104
|
-
policy, and reapplies the call, returning busy under persistent contention. A
|
|
105
|
-
revision_conflict means a revision you passed is stale: re-read the record
|
|
106
|
-
before deciding whether to retry.
|
|
107
|
-
A solo edit does not need writer acquisition; use it when concurrent checkout
|
|
108
|
-
writers need coordination. Record only observed facts and checks. Close-time
|
|
109
|
-
testing and verification gates accept only a configured check the CLI ran:
|
|
110
|
-
\`workit check <name>\` (\`npx -y @brainervirus/workit-cli check <name>\` when
|
|
111
|
-
\`workit\` is not on PATH); a recorded check result is a note and an ad-hoc
|
|
112
|
-
\`workit check -- <cmd>\` never satisfies a gate. Evidence can become stale when its bound candidate or tree changes; reconcile findings against the
|
|
113
|
-
current candidate before recording completion.
|
|
77
|
+
Workit is optional coordination and proof tooling around the host: you decide
|
|
78
|
+
what and whether, the \`workit\` CLI does how and records what it observed.
|
|
79
|
+
Native host allow/ask/deny, sandbox and org rules stay authoritative. Never
|
|
80
|
+
switch paths to evade a denial, never claim enforcement a host cannot provide,
|
|
81
|
+
and never fabricate a receipt, approval or verdict. A question answer is not
|
|
82
|
+
host permission, and imported decisions or handoffs never grant authority.
|
|
114
83
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
84
|
+
Autonomy contract:
|
|
85
|
+
- Continue to the requested endpoint (commit, push, PR, green CI, verified,
|
|
86
|
+
merged) without asking "continue?", running checks and repairs on the way.
|
|
87
|
+
Stop only for a new consequential choice, a host denial, a conflicting edit
|
|
88
|
+
or an unresolved blocker; report open gaps and uncertain workers.
|
|
89
|
+
- No endpoint named: stop at a local commit on a policy-compliant branch and
|
|
90
|
+
state the next command. Push and open a PR only when the request implies
|
|
91
|
+
delivery (fix, implement, ship) and \`workit grant show\` reports
|
|
92
|
+
\`defaultEndpoint\` \`pr\`; otherwise the endpoint is \`commit\`. PRs open,
|
|
93
|
+
CI green and verified is the most autonomy allows, never the default target.
|
|
94
|
+
Merge and release need a workspace grant; a blocked verb names its unblock.
|
|
95
|
+
- Ask only for a product or preference choice, or for authority you lack, with
|
|
96
|
+
your recommended answer. Facts are yours: read, run or prototype.
|
|
97
|
+
- Label claims measured (you ran it this session and saw the result),
|
|
98
|
+
inferred, or guess. Prose is a note: only a configured check the CLI ran
|
|
99
|
+
(\`workit check <name>\`) satisfies a gate; an ad-hoc
|
|
100
|
+
\`workit check -- <cmd>\` never does.
|
|
101
|
+
- Author is not verifier: never record a passing verdict on work your session
|
|
102
|
+
wrote. The lead starts each verifier with its own session
|
|
103
|
+
(\`WORKIT_SESSION_ID=<lead>-v<n>\`; Claude Code subagents get one from the
|
|
104
|
+
hook), and the verifier records \`workit ledger verdict\`.
|
|
105
|
+
- A local-commit endpoint does not imply PR readiness, and a local commit alone
|
|
106
|
+
is not evidence of a requested remote push. Before reporting delivery,
|
|
107
|
+
reconcile every requested item and observe it (\`workit verify-delivery\`).
|
|
124
108
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
109
|
+
CLI first: if a step has one right answer, use the \`workit\` verb instead of
|
|
110
|
+
hand-running git, gh or glab: check, git, pr, ci, stack, verify-delivery,
|
|
111
|
+
ledger, handoff, test-audit (\`workit help <verb>\`; without \`workit\` on
|
|
112
|
+
PATH, \`npx -y @brainervirus/workit-cli\`). \`busy\` is retryable
|
|
113
|
+
(\`workit doctor --fix-lock\` clears a dead lock); \`blocked\` names its unblock.
|
|
114
|
+
The task families (task, policy, evidence, finding, decision, worker, writer,
|
|
115
|
+
state) are optional continuity for tracked work; each branch has one implicit
|
|
116
|
+
task that the first recording creates, and a solo edit needs no task or writer.
|
|
117
|
+
Omit revisions; a revision_conflict means one you passed is stale, so re-read.
|
|
118
|
+
Across repositories, bind each item to its checkout, branch and endpoint,
|
|
119
|
+
resolve competing targets before a mutation, and reconcile an uncertain
|
|
120
|
+
external effect before retrying it.
|
|
129
121
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
host permission. Record a meaningful user choice once with provenance when
|
|
133
|
-
future retrieval helps. Never fabricate a receipt, re-ask only to mint one, or
|
|
134
|
-
let imported decisions grant authority. Never turn a host deny into allow or
|
|
135
|
-
claim enforcement/evidence a host cannot provide. A precise request with settled
|
|
136
|
-
constraints does not need an interview. Present genuine unresolved options with
|
|
137
|
-
evidence and a recommendation, then continue toward the requested delivery
|
|
138
|
-
endpoint: investigation, implementation, branch/commit/push, PR/MR-ready,
|
|
139
|
-
merge, or release. Run
|
|
140
|
-
applicable checks and safe repairs without repeated continuation questions;
|
|
141
|
-
stop for missing host authority, a new consequential choice, a conflicting edit,
|
|
142
|
-
or an unresolved blocker. A tracked record may close when actual evidence
|
|
143
|
-
supports its outcome; no final human closure ceremony is required.
|
|
122
|
+
Mid-task input: answer a quick question in place, fold an adjustment in, and
|
|
123
|
+
never silently drop or resume an objective (workit-continue).
|
|
144
124
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
Skill routing: slash aliases /wk-* load on demand. Use workit-steer for a
|
|
159
|
-
substantial interruption or change of direction, workit-deslop when relevant
|
|
160
|
-
to a PR-ready endpoint, workit-green-run for failing CI, workit-blast-radius
|
|
161
|
-
when impact is uncertain, workit-challenge for genuinely open consequential
|
|
162
|
-
choices, workit-bdd to turn acceptance criteria into Given/When/Then tests, and
|
|
163
|
-
workit-test-audit to check tests for tautologies. Load workit-plan when dependencies or handoff need durable next actions.
|
|
164
|
-
Load the skill; never act from memory of it.
|
|
125
|
+
Skills: load the skill, never act from memory of it (slash aliases /wk-<name>).
|
|
126
|
+
- brainstorm, plan, spec, grill, should we: workit-shape
|
|
127
|
+
- implement, build, add a feature: workit-implement
|
|
128
|
+
- review, blast radius: workit-review
|
|
129
|
+
- bug, broken, flaky, regression: workit-debug
|
|
130
|
+
- ship, babysit, CI, merge: workit-ship
|
|
131
|
+
- resume, pick up, handoff, interruption: workit-continue
|
|
132
|
+
- BDD, TDD, acceptance criteria, Given/When/Then: workit-bdd
|
|
133
|
+
- test audit, tautology, weak tests: workit-test-audit
|
|
134
|
+
- deslop, slop, dead code: workit-deslop
|
|
135
|
+
- fan out, parallelize, parallel agents, swarm: workit-fanout
|
|
136
|
+
- verify the app, smoke test, prove it works: workit-verify-app
|
|
165
137
|
`.trim();
|
|
166
138
|
|
|
167
139
|
// packages/workit-core/src/core/task-contract.ts
|
|
@@ -8507,16 +8479,6 @@ var stateOperations = {
|
|
|
8507
8479
|
expectedWorkspaceRevision: revision.nullable().optional(),
|
|
8508
8480
|
bundle: exportBundleSchema,
|
|
8509
8481
|
authorityRefs: array(refSchema)
|
|
8510
|
-
}),
|
|
8511
|
-
recover: operation({
|
|
8512
|
-
action: literal("recover"),
|
|
8513
|
-
taskId: id.optional(),
|
|
8514
|
-
expectedWorkspaceRevision: revision.optional(),
|
|
8515
|
-
target: _enum(["task", "workspace"]),
|
|
8516
|
-
expectedBytes: digest,
|
|
8517
|
-
snapshotDigest: digest,
|
|
8518
|
-
reason: text,
|
|
8519
|
-
authorityRefs: array(refSchema)
|
|
8520
8482
|
})
|
|
8521
8483
|
};
|
|
8522
8484
|
var operationSchemas = {
|
|
@@ -8529,10 +8491,17 @@ var operationSchemas = {
|
|
|
8529
8491
|
writer: discriminatedUnion("action", Object.values(writerOperations)),
|
|
8530
8492
|
state: discriminatedUnion("action", Object.values(stateOperations))
|
|
8531
8493
|
};
|
|
8532
|
-
var {
|
|
8494
|
+
var withImplicitTask = (options) => Object.values(options).map((option) => ("taskId" in option.shape) ? option.extend({ taskId: id.optional() }) : option);
|
|
8495
|
+
var advertised = (options) => discriminatedUnion("action", withImplicitTask(options));
|
|
8533
8496
|
var advertisedOperationSchemas = {
|
|
8534
|
-
|
|
8535
|
-
|
|
8497
|
+
task: advertised(taskOperations),
|
|
8498
|
+
policy: advertised(policyOperations),
|
|
8499
|
+
evidence: advertised(evidenceOperations),
|
|
8500
|
+
finding: advertised(findingOperations),
|
|
8501
|
+
decision: advertised(decisionOperations),
|
|
8502
|
+
worker: advertised(workerOperations),
|
|
8503
|
+
writer: advertised(writerOperations),
|
|
8504
|
+
state: advertised(stateOperations)
|
|
8536
8505
|
};
|
|
8537
8506
|
var compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
|
|
8538
8507
|
var success = (revisionValue, workspaceRevisionValue, data) => ({
|
|
@@ -8651,20 +8620,20 @@ function requirementId(input) {
|
|
|
8651
8620
|
}
|
|
8652
8621
|
|
|
8653
8622
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
8654
|
-
import { createHash as
|
|
8655
|
-
import * as
|
|
8656
|
-
import { spawnSync as
|
|
8657
|
-
import
|
|
8623
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
8624
|
+
import * as fs12 from "node:fs";
|
|
8625
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
8626
|
+
import path14 from "node:path";
|
|
8658
8627
|
|
|
8659
8628
|
// packages/workit-core/src/core/authority.ts
|
|
8660
|
-
import { createHash as
|
|
8661
|
-
import * as
|
|
8662
|
-
import
|
|
8629
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
8630
|
+
import * as fs9 from "node:fs";
|
|
8631
|
+
import path11 from "node:path";
|
|
8663
8632
|
|
|
8664
8633
|
// packages/workit-core/src/core/auto-approval.ts
|
|
8665
|
-
import { createHash as
|
|
8634
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
8666
8635
|
import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
|
|
8667
|
-
import
|
|
8636
|
+
import path10 from "node:path";
|
|
8668
8637
|
|
|
8669
8638
|
// packages/workit-core/src/core/workspaces.ts
|
|
8670
8639
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -9478,24 +9447,10 @@ var verifyPushIdentity = (root, provider, account) => {
|
|
|
9478
9447
|
};
|
|
9479
9448
|
|
|
9480
9449
|
// packages/workit-core/src/core/task-store.ts
|
|
9481
|
-
import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
|
|
9482
9450
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9483
|
-
import
|
|
9484
|
-
import
|
|
9485
|
-
|
|
9486
|
-
// packages/workit-core/src/core/package-root.ts
|
|
9487
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
9488
|
-
import path4 from "node:path";
|
|
9489
|
-
import { fileURLToPath } from "node:url";
|
|
9490
|
-
var packageRoot = () => {
|
|
9491
|
-
let dir = path4.dirname(fileURLToPath(import.meta.url));
|
|
9492
|
-
while (true) {
|
|
9493
|
-
const parent = path4.dirname(dir);
|
|
9494
|
-
if (existsSync2(path4.join(dir, "package.json")) || parent === dir)
|
|
9495
|
-
return dir;
|
|
9496
|
-
dir = parent;
|
|
9497
|
-
}
|
|
9498
|
-
};
|
|
9451
|
+
import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypto";
|
|
9452
|
+
import * as fs8 from "node:fs";
|
|
9453
|
+
import path9 from "node:path";
|
|
9499
9454
|
|
|
9500
9455
|
// node_modules/@openclaw/fs-safe/dist/errors.js
|
|
9501
9456
|
var OPERATIONAL_CODES = new Set([
|
|
@@ -9546,7 +9501,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
|
|
|
9546
9501
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
9547
9502
|
import { randomBytes } from "node:crypto";
|
|
9548
9503
|
import fsSync from "node:fs";
|
|
9549
|
-
import
|
|
9504
|
+
import path4 from "node:path";
|
|
9550
9505
|
|
|
9551
9506
|
// node_modules/@openclaw/fs-safe/dist/bounded-read.js
|
|
9552
9507
|
import fs2 from "node:fs";
|
|
@@ -9636,13 +9591,13 @@ ${ownershipToken}
|
|
|
9636
9591
|
return { raw, ownershipToken };
|
|
9637
9592
|
}
|
|
9638
9593
|
function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
9639
|
-
const resolved =
|
|
9640
|
-
const lexicalRelative =
|
|
9641
|
-
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${
|
|
9642
|
-
if (!relative || relative === ".." || relative.startsWith(`..${
|
|
9594
|
+
const resolved = path4.resolve(lockPath);
|
|
9595
|
+
const lexicalRelative = path4.relative(lockRoot.rootDir, resolved);
|
|
9596
|
+
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(lexicalRelative) ? lexicalRelative : path4.relative(lockRoot.rootReal, resolved);
|
|
9597
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path4.sep}`) || path4.isAbsolute(relative)) {
|
|
9643
9598
|
throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
9644
9599
|
}
|
|
9645
|
-
return relative.split(
|
|
9600
|
+
return relative.split(path4.sep).join(path4.posix.sep);
|
|
9646
9601
|
}
|
|
9647
9602
|
function parseSidecarLockPayload(raw, parser) {
|
|
9648
9603
|
if (parser) {
|
|
@@ -9789,7 +9744,7 @@ function getFsSafeLockConfig() {
|
|
|
9789
9744
|
|
|
9790
9745
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
9791
9746
|
import fs4 from "node:fs";
|
|
9792
|
-
import
|
|
9747
|
+
import path5 from "node:path";
|
|
9793
9748
|
|
|
9794
9749
|
// node_modules/@openclaw/fs-safe/dist/timing.js
|
|
9795
9750
|
function sleepSync(ms) {
|
|
@@ -9880,26 +9835,26 @@ function canonicalLockParentSync(parent) {
|
|
|
9880
9835
|
return process.platform === "win32" ? fs4.realpathSync.native(parent) : fs4.realpathSync(parent);
|
|
9881
9836
|
}
|
|
9882
9837
|
function normalizeTargetPath(targetPath) {
|
|
9883
|
-
const resolved =
|
|
9884
|
-
fs4.mkdirSync(
|
|
9838
|
+
const resolved = path5.resolve(targetPath);
|
|
9839
|
+
fs4.mkdirSync(path5.dirname(resolved), { recursive: true });
|
|
9885
9840
|
try {
|
|
9886
|
-
return
|
|
9841
|
+
return path5.join(canonicalLockParentSync(path5.dirname(resolved)), path5.basename(resolved));
|
|
9887
9842
|
} catch {
|
|
9888
9843
|
return resolved;
|
|
9889
9844
|
}
|
|
9890
9845
|
}
|
|
9891
9846
|
function boundedLockPath(lockPath, lockRoot) {
|
|
9892
|
-
const resolved =
|
|
9847
|
+
const resolved = path5.resolve(lockPath);
|
|
9893
9848
|
if (!lockRoot)
|
|
9894
9849
|
return resolved;
|
|
9895
9850
|
relativeSidecarLockPath(lockRoot, resolved);
|
|
9896
|
-
const parent =
|
|
9851
|
+
const parent = path5.dirname(resolved);
|
|
9897
9852
|
const parentReal = canonicalLockParentSync(parent);
|
|
9898
|
-
const parentRelative =
|
|
9899
|
-
if (parentRelative === ".." || parentRelative.startsWith(`..${
|
|
9853
|
+
const parentRelative = path5.relative(lockRoot.rootReal, parentReal);
|
|
9854
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path5.sep}`) || path5.isAbsolute(parentRelative)) {
|
|
9900
9855
|
throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
|
|
9901
9856
|
}
|
|
9902
|
-
return
|
|
9857
|
+
return path5.join(parentReal, path5.basename(resolved));
|
|
9903
9858
|
}
|
|
9904
9859
|
function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
9905
9860
|
const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
|
|
@@ -10098,10 +10053,306 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
10098
10053
|
}
|
|
10099
10054
|
}
|
|
10100
10055
|
}
|
|
10056
|
+
// packages/workit-core/src/core/package-root.ts
|
|
10057
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
10058
|
+
import path6 from "node:path";
|
|
10059
|
+
import { fileURLToPath } from "node:url";
|
|
10060
|
+
var packageRoot = () => {
|
|
10061
|
+
let dir = path6.dirname(fileURLToPath(import.meta.url));
|
|
10062
|
+
while (true) {
|
|
10063
|
+
const parent = path6.dirname(dir);
|
|
10064
|
+
if (existsSync2(path6.join(dir, "package.json")) || parent === dir)
|
|
10065
|
+
return dir;
|
|
10066
|
+
dir = parent;
|
|
10067
|
+
}
|
|
10068
|
+
};
|
|
10069
|
+
|
|
10101
10070
|
// packages/workit-core/src/core/store-lock.ts
|
|
10071
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
10072
|
+
import fs6 from "node:fs";
|
|
10073
|
+
import { hostname } from "node:os";
|
|
10074
|
+
|
|
10075
|
+
// packages/workit-core/src/store/paths.ts
|
|
10102
10076
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
10077
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
10103
10078
|
import fs5 from "node:fs";
|
|
10104
|
-
import
|
|
10079
|
+
import path7 from "node:path";
|
|
10080
|
+
var NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
|
|
10081
|
+
var gitRun = (cwd, args) => {
|
|
10082
|
+
const run = spawnSync2("git", args, {
|
|
10083
|
+
cwd,
|
|
10084
|
+
encoding: "utf8",
|
|
10085
|
+
timeout: 20000,
|
|
10086
|
+
killSignal: "SIGKILL",
|
|
10087
|
+
windowsHide: true,
|
|
10088
|
+
env: {
|
|
10089
|
+
...process.env,
|
|
10090
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
10091
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
10092
|
+
LC_ALL: "C",
|
|
10093
|
+
LANGUAGE: "C"
|
|
10094
|
+
}
|
|
10095
|
+
});
|
|
10096
|
+
const error = run.error;
|
|
10097
|
+
return {
|
|
10098
|
+
ok: run.status === 0,
|
|
10099
|
+
stdout: run.stdout ?? "",
|
|
10100
|
+
stderr: error ? String(error.message) : run.stderr ?? "",
|
|
10101
|
+
missing: error?.code === "ENOENT"
|
|
10102
|
+
};
|
|
10103
|
+
};
|
|
10104
|
+
var shortHash = (value) => createHash4("sha256").update(value).digest("hex").slice(0, 12);
|
|
10105
|
+
var real = (file) => {
|
|
10106
|
+
try {
|
|
10107
|
+
return fs5.realpathSync.native(file);
|
|
10108
|
+
} catch {
|
|
10109
|
+
return file;
|
|
10110
|
+
}
|
|
10111
|
+
};
|
|
10112
|
+
var DISCOVERY_ENV = [
|
|
10113
|
+
"GIT_DIR",
|
|
10114
|
+
"GIT_COMMON_DIR",
|
|
10115
|
+
"GIT_WORK_TREE",
|
|
10116
|
+
"GIT_CEILING_DIRECTORIES",
|
|
10117
|
+
"GIT_DISCOVERY_ACROSS_FILESYSTEM"
|
|
10118
|
+
];
|
|
10119
|
+
var isGitDir = (dir) => {
|
|
10120
|
+
try {
|
|
10121
|
+
return fs5.statSync(path7.join(dir, "HEAD")).isFile() && fs5.statSync(path7.join(dir, "objects")).isDirectory() && fs5.statSync(path7.join(dir, "refs")).isDirectory();
|
|
10122
|
+
} catch {
|
|
10123
|
+
return false;
|
|
10124
|
+
}
|
|
10125
|
+
};
|
|
10126
|
+
var reftableRefs = (common) => {
|
|
10127
|
+
try {
|
|
10128
|
+
return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs5.readFileSync(path7.join(common, "config"), "utf8"));
|
|
10129
|
+
} catch {
|
|
10130
|
+
return false;
|
|
10131
|
+
}
|
|
10132
|
+
};
|
|
10133
|
+
function fastGit(root) {
|
|
10134
|
+
if (DISCOVERY_ENV.some((name) => process.env[name]))
|
|
10135
|
+
return null;
|
|
10136
|
+
let dir = real(root);
|
|
10137
|
+
let device = null;
|
|
10138
|
+
for (;; ) {
|
|
10139
|
+
let stat;
|
|
10140
|
+
try {
|
|
10141
|
+
stat = fs5.statSync(dir);
|
|
10142
|
+
} catch {
|
|
10143
|
+
return null;
|
|
10144
|
+
}
|
|
10145
|
+
if (device !== null && stat.dev !== device)
|
|
10146
|
+
return { kind: "none" };
|
|
10147
|
+
device = stat.dev;
|
|
10148
|
+
const dotGit = path7.join(dir, ".git");
|
|
10149
|
+
let entry = null;
|
|
10150
|
+
try {
|
|
10151
|
+
entry = fs5.statSync(dotGit);
|
|
10152
|
+
} catch {}
|
|
10153
|
+
let gitDir = null;
|
|
10154
|
+
let top = null;
|
|
10155
|
+
if (entry) {
|
|
10156
|
+
top = dir;
|
|
10157
|
+
if (entry.isDirectory())
|
|
10158
|
+
gitDir = dotGit;
|
|
10159
|
+
else if (entry.isFile()) {
|
|
10160
|
+
let text = "";
|
|
10161
|
+
try {
|
|
10162
|
+
text = fs5.readFileSync(dotGit, "utf8");
|
|
10163
|
+
} catch {
|
|
10164
|
+
return null;
|
|
10165
|
+
}
|
|
10166
|
+
const match = /^gitdir: (.+)$/mu.exec(text);
|
|
10167
|
+
if (!match)
|
|
10168
|
+
return null;
|
|
10169
|
+
gitDir = path7.resolve(dir, match[1].trim());
|
|
10170
|
+
} else
|
|
10171
|
+
return null;
|
|
10172
|
+
if (!fs5.existsSync(path7.join(gitDir, "HEAD")))
|
|
10173
|
+
return null;
|
|
10174
|
+
} else if (isGitDir(dir))
|
|
10175
|
+
gitDir = dir;
|
|
10176
|
+
if (gitDir) {
|
|
10177
|
+
let common = gitDir;
|
|
10178
|
+
try {
|
|
10179
|
+
common = path7.resolve(gitDir, fs5.readFileSync(path7.join(gitDir, "commondir"), "utf8").trim());
|
|
10180
|
+
} catch {}
|
|
10181
|
+
if (!fs5.existsSync(path7.join(common, "objects")))
|
|
10182
|
+
return null;
|
|
10183
|
+
return {
|
|
10184
|
+
kind: "repo",
|
|
10185
|
+
common: real(common),
|
|
10186
|
+
gitDir: real(gitDir),
|
|
10187
|
+
top: top === null ? null : real(top),
|
|
10188
|
+
reftable: reftableRefs(common)
|
|
10189
|
+
};
|
|
10190
|
+
}
|
|
10191
|
+
const parent = path7.dirname(dir);
|
|
10192
|
+
if (parent === dir)
|
|
10193
|
+
return { kind: "none" };
|
|
10194
|
+
dir = parent;
|
|
10195
|
+
}
|
|
10196
|
+
}
|
|
10197
|
+
function resolveStore(root) {
|
|
10198
|
+
root = path7.resolve(root);
|
|
10199
|
+
let directory = false;
|
|
10200
|
+
try {
|
|
10201
|
+
directory = fs5.statSync(root).isDirectory();
|
|
10202
|
+
} catch {}
|
|
10203
|
+
if (!directory)
|
|
10204
|
+
return { dir: path7.join(root, ".workit"), shared: false, top: null };
|
|
10205
|
+
const fast = fastGit(root);
|
|
10206
|
+
if (fast?.kind === "none")
|
|
10207
|
+
return { dir: path7.join(root, ".workit"), shared: false, top: null };
|
|
10208
|
+
if (fast?.kind === "repo")
|
|
10209
|
+
return {
|
|
10210
|
+
dir: path7.join(fast.common, "workit"),
|
|
10211
|
+
shared: true,
|
|
10212
|
+
top: fast.top,
|
|
10213
|
+
git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
|
|
10214
|
+
};
|
|
10215
|
+
const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
10216
|
+
const common = run.stdout.trim();
|
|
10217
|
+
if (run.ok && common) {
|
|
10218
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
10219
|
+
return {
|
|
10220
|
+
dir: path7.join(real(path7.resolve(root, common)), "workit"),
|
|
10221
|
+
shared: true,
|
|
10222
|
+
top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
|
|
10223
|
+
};
|
|
10224
|
+
}
|
|
10225
|
+
if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
|
|
10226
|
+
return { dir: path7.join(root, ".workit"), shared: false, top: null };
|
|
10227
|
+
return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
|
|
10228
|
+
`)[0] || "no output"}); fix the repository (git status should work here)`);
|
|
10229
|
+
}
|
|
10230
|
+
var rebasingBranch = (gitDir) => {
|
|
10231
|
+
for (const state of ["rebase-merge", "rebase-apply"]) {
|
|
10232
|
+
try {
|
|
10233
|
+
const ref = fs5.readFileSync(path7.join(gitDir, state, "head-name"), "utf8").trim();
|
|
10234
|
+
const match = /^refs\/heads\/(.+)$/u.exec(ref);
|
|
10235
|
+
if (match)
|
|
10236
|
+
return match[1];
|
|
10237
|
+
} catch {}
|
|
10238
|
+
}
|
|
10239
|
+
return null;
|
|
10240
|
+
};
|
|
10241
|
+
function resolveTaskKey(root, location) {
|
|
10242
|
+
if (!location.shared)
|
|
10243
|
+
return { key: `dir-${shortHash(root)}`, kind: "dir", branch: null };
|
|
10244
|
+
const branchKey = (name) => ({ key: name, kind: "branch", branch: name });
|
|
10245
|
+
if (location.git && !location.git.reftable) {
|
|
10246
|
+
let head = "";
|
|
10247
|
+
try {
|
|
10248
|
+
head = fs5.readFileSync(path7.join(location.git.gitDir, "HEAD"), "utf8").trim();
|
|
10249
|
+
} catch {}
|
|
10250
|
+
const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
|
|
10251
|
+
if (ref && ref[1] !== ".invalid")
|
|
10252
|
+
return branchKey(ref[1]);
|
|
10253
|
+
if (/^[0-9a-f]{40,64}$/u.test(head)) {
|
|
10254
|
+
const rebasing = rebasingBranch(location.git.gitDir);
|
|
10255
|
+
if (rebasing)
|
|
10256
|
+
return branchKey(rebasing);
|
|
10257
|
+
return {
|
|
10258
|
+
key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
|
|
10259
|
+
kind: "detached",
|
|
10260
|
+
branch: null
|
|
10261
|
+
};
|
|
10262
|
+
}
|
|
10263
|
+
}
|
|
10264
|
+
const branch = gitRun(root, ["symbolic-ref", "-q", "--short", "HEAD"]);
|
|
10265
|
+
const name = branch.ok ? branch.stdout.trim() : "";
|
|
10266
|
+
if (name && name !== ".invalid")
|
|
10267
|
+
return branchKey(name);
|
|
10268
|
+
const gitDir = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
|
|
10269
|
+
if (gitDir.ok && gitDir.stdout.trim()) {
|
|
10270
|
+
const rebasing = rebasingBranch(gitDir.stdout.trim());
|
|
10271
|
+
if (rebasing)
|
|
10272
|
+
return branchKey(rebasing);
|
|
10273
|
+
}
|
|
10274
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
10275
|
+
const worktree = top.ok && top.stdout.trim() ? top.stdout.trim() : root;
|
|
10276
|
+
return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
|
|
10277
|
+
}
|
|
10278
|
+
function checkoutRootOf(root, location) {
|
|
10279
|
+
const resolved = path7.resolve(root);
|
|
10280
|
+
let canonical = resolved;
|
|
10281
|
+
try {
|
|
10282
|
+
canonical = fs5.realpathSync(resolved);
|
|
10283
|
+
} catch {
|
|
10284
|
+
return resolved;
|
|
10285
|
+
}
|
|
10286
|
+
const found = location ?? resolveStore(canonical);
|
|
10287
|
+
if (found instanceof Error || !found.shared || !found.top)
|
|
10288
|
+
return canonical;
|
|
10289
|
+
if (real(canonical) === found.top)
|
|
10290
|
+
return canonical;
|
|
10291
|
+
try {
|
|
10292
|
+
return fs5.realpathSync(found.top);
|
|
10293
|
+
} catch {
|
|
10294
|
+
return canonical;
|
|
10295
|
+
}
|
|
10296
|
+
}
|
|
10297
|
+
function branchRenames(location) {
|
|
10298
|
+
if (!location.shared)
|
|
10299
|
+
return [];
|
|
10300
|
+
const files = [];
|
|
10301
|
+
const walk = (dir) => {
|
|
10302
|
+
let entries;
|
|
10303
|
+
try {
|
|
10304
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
10305
|
+
} catch {
|
|
10306
|
+
return;
|
|
10307
|
+
}
|
|
10308
|
+
for (const entry of entries)
|
|
10309
|
+
if (entry.isDirectory())
|
|
10310
|
+
walk(path7.join(dir, entry.name));
|
|
10311
|
+
else
|
|
10312
|
+
files.push(path7.join(dir, entry.name));
|
|
10313
|
+
};
|
|
10314
|
+
walk(path7.join(path7.dirname(location.dir), "logs", "refs", "heads"));
|
|
10315
|
+
if (location.git)
|
|
10316
|
+
files.push(path7.join(location.git.gitDir, "logs", "HEAD"));
|
|
10317
|
+
const seen = new Set;
|
|
10318
|
+
const renames = [];
|
|
10319
|
+
for (const file of files) {
|
|
10320
|
+
let text = "";
|
|
10321
|
+
try {
|
|
10322
|
+
text = fs5.readFileSync(file, "utf8");
|
|
10323
|
+
} catch {
|
|
10324
|
+
continue;
|
|
10325
|
+
}
|
|
10326
|
+
for (const match of text.matchAll(/ (\d+) [+-]\d{4}\tBranch: renamed refs\/heads\/(\S+) to refs\/heads\/(\S+)/gu)) {
|
|
10327
|
+
const rename = { from: match[2], to: match[3], at: Number(match[1]) };
|
|
10328
|
+
const id = `${rename.at}\x00${rename.from}\x00${rename.to}`;
|
|
10329
|
+
if (seen.has(id))
|
|
10330
|
+
continue;
|
|
10331
|
+
seen.add(id);
|
|
10332
|
+
renames.push(rename);
|
|
10333
|
+
}
|
|
10334
|
+
}
|
|
10335
|
+
return renames.toSorted((left, right) => left.at - right.at);
|
|
10336
|
+
}
|
|
10337
|
+
function followRenames(key, boundAt, renames) {
|
|
10338
|
+
let current = key;
|
|
10339
|
+
let since = Math.floor(Date.parse(boundAt) / 1000);
|
|
10340
|
+
for (const rename of renames)
|
|
10341
|
+
if (rename.from === current && rename.at >= since) {
|
|
10342
|
+
current = rename.to;
|
|
10343
|
+
since = rename.at;
|
|
10344
|
+
}
|
|
10345
|
+
return current;
|
|
10346
|
+
}
|
|
10347
|
+
var checkoutSlug = (root) => {
|
|
10348
|
+
let canonical = real(root);
|
|
10349
|
+
if (process.platform === "win32")
|
|
10350
|
+
canonical = canonical.toLowerCase();
|
|
10351
|
+
const base = path7.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
|
|
10352
|
+
return `${base}-${shortHash(canonical)}`;
|
|
10353
|
+
};
|
|
10354
|
+
|
|
10355
|
+
// packages/workit-core/src/core/store-lock.ts
|
|
10105
10356
|
var metadataLockSchema = object({
|
|
10106
10357
|
pid: number2().int().nonnegative().safe(),
|
|
10107
10358
|
processStart: string2().nullable(),
|
|
@@ -10133,10 +10384,6 @@ var parseMetadataLockOrNull = (raw) => {
|
|
|
10133
10384
|
return null;
|
|
10134
10385
|
}
|
|
10135
10386
|
};
|
|
10136
|
-
var sameMetadataLock = (left, right) => {
|
|
10137
|
-
const parsed = metadataLockSchema.safeParse(left);
|
|
10138
|
-
return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
|
|
10139
|
-
};
|
|
10140
10387
|
var parseProcStatStart = (stat) => {
|
|
10141
10388
|
const close = stat.lastIndexOf(")");
|
|
10142
10389
|
if (close < 0)
|
|
@@ -10146,14 +10393,14 @@ var parseProcStatStart = (stat) => {
|
|
|
10146
10393
|
var processStartOf = (pid) => {
|
|
10147
10394
|
if (process.platform === "linux") {
|
|
10148
10395
|
try {
|
|
10149
|
-
return parseProcStatStart(
|
|
10396
|
+
return parseProcStatStart(fs6.readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
10150
10397
|
} catch {
|
|
10151
10398
|
return null;
|
|
10152
10399
|
}
|
|
10153
10400
|
}
|
|
10154
10401
|
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
10155
10402
|
try {
|
|
10156
|
-
const run =
|
|
10403
|
+
const run = spawnSync3("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
10157
10404
|
encoding: "utf8",
|
|
10158
10405
|
timeout: 1000
|
|
10159
10406
|
});
|
|
@@ -10167,7 +10414,7 @@ var processStartOf = (pid) => {
|
|
|
10167
10414
|
};
|
|
10168
10415
|
var readTrimmed = (file) => {
|
|
10169
10416
|
try {
|
|
10170
|
-
return
|
|
10417
|
+
return fs6.readFileSync(file, "utf8").trim() || null;
|
|
10171
10418
|
} catch {
|
|
10172
10419
|
return null;
|
|
10173
10420
|
}
|
|
@@ -10178,7 +10425,7 @@ var localLockHost = () => {
|
|
|
10178
10425
|
return cachedLockHost;
|
|
10179
10426
|
let pidns = null;
|
|
10180
10427
|
try {
|
|
10181
|
-
pidns = /\[(\d+)\]/.exec(
|
|
10428
|
+
pidns = /\[(\d+)\]/.exec(fs6.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
|
|
10182
10429
|
} catch {}
|
|
10183
10430
|
const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
|
|
10184
10431
|
cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
|
|
@@ -10218,7 +10465,7 @@ var classifyLockOwner = (payload, ageMs, localHost = localLockHost()) => {
|
|
|
10218
10465
|
};
|
|
10219
10466
|
var ageOf = (file, nowMs) => {
|
|
10220
10467
|
try {
|
|
10221
|
-
return nowMs -
|
|
10468
|
+
return nowMs - fs6.lstatSync(file).mtimeMs;
|
|
10222
10469
|
} catch {
|
|
10223
10470
|
return null;
|
|
10224
10471
|
}
|
|
@@ -10229,117 +10476,151 @@ var clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
|
|
|
10229
10476
|
if (age === null || age <= RECLAIM_GUARD_TTL_MS)
|
|
10230
10477
|
return false;
|
|
10231
10478
|
try {
|
|
10232
|
-
|
|
10479
|
+
fs6.rmdirSync(guard);
|
|
10233
10480
|
return true;
|
|
10234
10481
|
} catch {
|
|
10235
10482
|
return false;
|
|
10236
10483
|
}
|
|
10237
10484
|
};
|
|
10238
10485
|
|
|
10239
|
-
// packages/workit-core/src/
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
var
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
var hostSession = (value) => isObject2(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
10251
|
-
var indexEntry = (task, file) => {
|
|
10252
|
-
const sessions = [];
|
|
10253
|
-
const intent = hostSession(task.intent.provenance.session);
|
|
10254
|
-
if (intent)
|
|
10255
|
-
sessions.push({ ...intent, workerId: null });
|
|
10256
|
-
for (const worker of task.workers) {
|
|
10257
|
-
const session = hostSession(worker.data.session);
|
|
10258
|
-
if (session)
|
|
10259
|
-
sessions.push({ ...session, workerId: worker.id });
|
|
10486
|
+
// packages/workit-core/src/store/event-log.ts
|
|
10487
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10488
|
+
import fs7 from "node:fs";
|
|
10489
|
+
import path8 from "node:path";
|
|
10490
|
+
var EVENT_VERSION = 1;
|
|
10491
|
+
|
|
10492
|
+
class LogDamage extends Error {
|
|
10493
|
+
upgrade;
|
|
10494
|
+
constructor(message, upgrade = false) {
|
|
10495
|
+
super(message);
|
|
10496
|
+
this.upgrade = upgrade;
|
|
10260
10497
|
}
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
createdAt: task.createdAt,
|
|
10266
|
-
updatedAt: task.updatedAt,
|
|
10267
|
-
objective: task.intent.data.objective,
|
|
10268
|
-
source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
|
|
10269
|
-
progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
|
|
10270
|
-
sessions,
|
|
10271
|
-
file
|
|
10272
|
-
};
|
|
10273
|
-
};
|
|
10274
|
-
var fileSignature = (file) => {
|
|
10498
|
+
}
|
|
10499
|
+
var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10500
|
+
var parseEvent = (line, file, offset) => {
|
|
10501
|
+
let value;
|
|
10275
10502
|
try {
|
|
10276
|
-
|
|
10277
|
-
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
10503
|
+
value = JSON.parse(line);
|
|
10278
10504
|
} catch {
|
|
10279
|
-
|
|
10280
|
-
}
|
|
10281
|
-
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
|
|
10505
|
+
throw new LogDamage(`${file}: unreadable event at byte ${offset}`);
|
|
10506
|
+
}
|
|
10507
|
+
if (!isObject2(value) || typeof value.v !== "number")
|
|
10508
|
+
throw new LogDamage(`${file}: malformed event at byte ${offset}`);
|
|
10509
|
+
if (value.v > EVENT_VERSION)
|
|
10510
|
+
throw new LogDamage(`${file}: event format v${value.v} was written by a newer Workit; upgrade Workit to read this task`, true);
|
|
10511
|
+
if (typeof value.seq !== "number" || !Number.isInteger(value.seq) || typeof value.type !== "string" || typeof value.id !== "string" || typeof value.task !== "string" || !isObject2(value.data))
|
|
10512
|
+
throw new LogDamage(`${file}: malformed event at byte ${offset}`);
|
|
10513
|
+
return value;
|
|
10287
10514
|
};
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10515
|
+
function readLog(file, from = 0, expectSeq = null) {
|
|
10516
|
+
let fd;
|
|
10517
|
+
try {
|
|
10518
|
+
fd = fs7.openSync(file, "r");
|
|
10519
|
+
} catch (error) {
|
|
10520
|
+
if (error.code === "ENOENT")
|
|
10521
|
+
return null;
|
|
10522
|
+
throw error;
|
|
10523
|
+
}
|
|
10524
|
+
let bytes;
|
|
10525
|
+
let size;
|
|
10526
|
+
try {
|
|
10527
|
+
size = fs7.fstatSync(fd).size;
|
|
10528
|
+
if (from > size)
|
|
10529
|
+
throw new LogDamage(`${file}: shorter than its snapshot`);
|
|
10530
|
+
bytes = Buffer.alloc(size - from);
|
|
10531
|
+
let read = 0;
|
|
10532
|
+
while (read < bytes.length) {
|
|
10533
|
+
const got = fs7.readSync(fd, bytes, read, bytes.length - read, from + read);
|
|
10534
|
+
if (got === 0)
|
|
10535
|
+
break;
|
|
10536
|
+
read += got;
|
|
10537
|
+
}
|
|
10538
|
+
bytes = bytes.subarray(0, read);
|
|
10539
|
+
size = from + read;
|
|
10540
|
+
} finally {
|
|
10541
|
+
fs7.closeSync(fd);
|
|
10542
|
+
}
|
|
10543
|
+
const events = [];
|
|
10544
|
+
let start = 0;
|
|
10545
|
+
let lastStart = from;
|
|
10546
|
+
let seq = expectSeq;
|
|
10547
|
+
for (;; ) {
|
|
10548
|
+
const newline = bytes.indexOf(10, start);
|
|
10549
|
+
if (newline < 0)
|
|
10550
|
+
break;
|
|
10551
|
+
const line = bytes.subarray(start, newline).toString("utf8");
|
|
10552
|
+
if (line.trim() !== "") {
|
|
10553
|
+
const event = parseEvent(line, file, from + start);
|
|
10554
|
+
if (seq !== null && event.seq !== seq)
|
|
10555
|
+
throw new LogDamage(`${file}: event seq ${event.seq} where ${seq} was expected`);
|
|
10556
|
+
seq = event.seq + 1;
|
|
10557
|
+
events.push(event);
|
|
10558
|
+
lastStart = from + start;
|
|
10559
|
+
}
|
|
10560
|
+
start = newline + 1;
|
|
10561
|
+
}
|
|
10562
|
+
return { events, end: from + start, lastStart, size };
|
|
10563
|
+
}
|
|
10564
|
+
var fsyncDirectory = (dir) => {
|
|
10565
|
+
try {
|
|
10566
|
+
const fd = fs7.openSync(dir, "r");
|
|
10292
10567
|
try {
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
cachedRuntimeVersion = "0.0.0";
|
|
10568
|
+
fs7.fsyncSync(fd);
|
|
10569
|
+
} finally {
|
|
10570
|
+
fs7.closeSync(fd);
|
|
10297
10571
|
}
|
|
10572
|
+
} catch (error) {
|
|
10573
|
+
if (process.platform !== "win32")
|
|
10574
|
+
throw error;
|
|
10298
10575
|
}
|
|
10299
|
-
return cachedRuntimeVersion;
|
|
10300
10576
|
};
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10311
|
-
|
|
10312
|
-
|
|
10577
|
+
function appendEvent(file, event, validEnd) {
|
|
10578
|
+
const line = Buffer.from(`${JSON.stringify(event)}
|
|
10579
|
+
`, "utf8");
|
|
10580
|
+
const created = !fs7.existsSync(file);
|
|
10581
|
+
if (!created && fs7.statSync(file).size !== validEnd) {
|
|
10582
|
+
const cut = fs7.openSync(file, "r+");
|
|
10583
|
+
try {
|
|
10584
|
+
fs7.ftruncateSync(cut, validEnd);
|
|
10585
|
+
fs7.fsyncSync(cut);
|
|
10586
|
+
} finally {
|
|
10587
|
+
fs7.closeSync(cut);
|
|
10588
|
+
}
|
|
10313
10589
|
}
|
|
10314
|
-
|
|
10315
|
-
};
|
|
10316
|
-
var jsonBytes = (value) => `${canonicalJson(value)}
|
|
10317
|
-
`;
|
|
10318
|
-
var digestBytes = (value) => createHash4("sha256").update(value).digest("hex");
|
|
10319
|
-
var isObject2 = (value) => typeof value === "object" && value !== null;
|
|
10320
|
-
var validId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
10321
|
-
var validDigest = (value) => /^[0-9a-f]{64}$/.test(value);
|
|
10322
|
-
var sameDirectoryIdentity = (left, right) => {
|
|
10323
|
-
if (!path7.isAbsolute(left) || !path7.isAbsolute(right))
|
|
10324
|
-
return false;
|
|
10325
|
-
const normalizedLeft = path7.resolve(left);
|
|
10326
|
-
const normalizedRight = path7.resolve(right);
|
|
10590
|
+
const fd = fs7.openSync(file, "a", 384);
|
|
10327
10591
|
try {
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
const canonicalLeft = fs6.realpathSync(normalizedLeft);
|
|
10335
|
-
const canonicalRight = fs6.realpathSync(normalizedRight);
|
|
10336
|
-
if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
|
|
10337
|
-
return true;
|
|
10338
|
-
return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
|
|
10339
|
-
} catch {
|
|
10340
|
-
return false;
|
|
10592
|
+
let written = 0;
|
|
10593
|
+
while (written < line.length)
|
|
10594
|
+
written += fs7.writeSync(fd, line, written, line.length - written);
|
|
10595
|
+
fs7.fsyncSync(fd);
|
|
10596
|
+
} finally {
|
|
10597
|
+
fs7.closeSync(fd);
|
|
10341
10598
|
}
|
|
10342
|
-
|
|
10599
|
+
if (created)
|
|
10600
|
+
fsyncDirectory(path8.dirname(file));
|
|
10601
|
+
return validEnd;
|
|
10602
|
+
}
|
|
10603
|
+
function replaceFile(file, bytes, durable) {
|
|
10604
|
+
const temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
10605
|
+
try {
|
|
10606
|
+
const fd = fs7.openSync(temporary, "wx", 384);
|
|
10607
|
+
try {
|
|
10608
|
+
fs7.writeSync(fd, bytes);
|
|
10609
|
+
if (durable)
|
|
10610
|
+
fs7.fsyncSync(fd);
|
|
10611
|
+
} finally {
|
|
10612
|
+
fs7.closeSync(fd);
|
|
10613
|
+
}
|
|
10614
|
+
renameWithRetry(temporary, file);
|
|
10615
|
+
if (durable)
|
|
10616
|
+
fsyncDirectory(path8.dirname(file));
|
|
10617
|
+
} catch (error) {
|
|
10618
|
+
try {
|
|
10619
|
+
fs7.unlinkSync(temporary);
|
|
10620
|
+
} catch {}
|
|
10621
|
+
throw error;
|
|
10622
|
+
}
|
|
10623
|
+
}
|
|
10343
10624
|
var TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
10344
10625
|
var isTransientWindowsError = (error) => {
|
|
10345
10626
|
if (process.platform !== "win32")
|
|
@@ -10360,138 +10641,596 @@ var retryTransient = (run) => {
|
|
|
10360
10641
|
}
|
|
10361
10642
|
}
|
|
10362
10643
|
};
|
|
10644
|
+
var renameWithRetry = (from, to) => retryTransient(() => fs7.renameSync(from, to));
|
|
10645
|
+
|
|
10646
|
+
// packages/workit-core/src/store/patch.ts
|
|
10647
|
+
var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10648
|
+
var equal = (left, right) => {
|
|
10649
|
+
if (left === right)
|
|
10650
|
+
return true;
|
|
10651
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null)
|
|
10652
|
+
return false;
|
|
10653
|
+
if (Array.isArray(left) !== Array.isArray(right))
|
|
10654
|
+
return false;
|
|
10655
|
+
if (Array.isArray(left)) {
|
|
10656
|
+
const other = right;
|
|
10657
|
+
return left.length === other.length && left.every((item, index) => equal(item, other[index]));
|
|
10658
|
+
}
|
|
10659
|
+
const leftKeys = Object.keys(left).filter((key) => left[key] !== undefined);
|
|
10660
|
+
const rightKeys = Object.keys(right).filter((key) => right[key] !== undefined);
|
|
10661
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => equal(left[key], right[key]));
|
|
10662
|
+
};
|
|
10663
|
+
function diff(before, after, path = []) {
|
|
10664
|
+
if (equal(before, after))
|
|
10665
|
+
return [];
|
|
10666
|
+
if (isPlainObject2(before) && isPlainObject2(after)) {
|
|
10667
|
+
const ops = [];
|
|
10668
|
+
for (const key of Object.keys(before))
|
|
10669
|
+
if (before[key] !== undefined && after[key] === undefined)
|
|
10670
|
+
ops.push({ op: "del", path: [...path, key] });
|
|
10671
|
+
for (const key of Object.keys(after)) {
|
|
10672
|
+
if (after[key] === undefined)
|
|
10673
|
+
continue;
|
|
10674
|
+
if (before[key] === undefined)
|
|
10675
|
+
ops.push({ op: "set", path: [...path, key], value: after[key] });
|
|
10676
|
+
else
|
|
10677
|
+
ops.push(...diff(before[key], after[key], [...path, key]));
|
|
10678
|
+
}
|
|
10679
|
+
return ops;
|
|
10680
|
+
}
|
|
10681
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
10682
|
+
const ops = [];
|
|
10683
|
+
if (after.length < before.length)
|
|
10684
|
+
ops.push({ op: "cut", path, length: after.length });
|
|
10685
|
+
const shared = Math.min(before.length, after.length);
|
|
10686
|
+
let changed = 0;
|
|
10687
|
+
for (let index = 0;index < shared; index += 1)
|
|
10688
|
+
if (!equal(before[index], after[index]))
|
|
10689
|
+
changed += 1;
|
|
10690
|
+
if (shared > 0 && changed > shared / 2 && changed > 4)
|
|
10691
|
+
return [{ op: "set", path, value: after }];
|
|
10692
|
+
for (let index = 0;index < shared; index += 1)
|
|
10693
|
+
ops.push(...diff(before[index], after[index], [...path, index]));
|
|
10694
|
+
if (after.length > before.length)
|
|
10695
|
+
ops.push({ op: "push", path, values: after.slice(before.length) });
|
|
10696
|
+
return ops;
|
|
10697
|
+
}
|
|
10698
|
+
return [{ op: "set", path, value: after }];
|
|
10699
|
+
}
|
|
10700
|
+
var clone2 = (value) => structuredClone(value);
|
|
10701
|
+
function apply(target, ops) {
|
|
10702
|
+
let root = target;
|
|
10703
|
+
for (const op of ops) {
|
|
10704
|
+
if (op.path.length === 0) {
|
|
10705
|
+
if (op.op === "set")
|
|
10706
|
+
root = clone2(op.value);
|
|
10707
|
+
else if (op.op === "push" && Array.isArray(root))
|
|
10708
|
+
root.push(...clone2(op.values));
|
|
10709
|
+
else if (op.op === "cut" && Array.isArray(root))
|
|
10710
|
+
root.length = op.length;
|
|
10711
|
+
else
|
|
10712
|
+
throw new Error(`patch: ${op.op} at the root is invalid`);
|
|
10713
|
+
continue;
|
|
10714
|
+
}
|
|
10715
|
+
let parent = root;
|
|
10716
|
+
for (const key of op.path.slice(0, -1)) {
|
|
10717
|
+
if (typeof parent !== "object" || parent === null)
|
|
10718
|
+
throw new Error(`patch: missing path ${op.path.join(".")}`);
|
|
10719
|
+
parent = parent[key];
|
|
10720
|
+
}
|
|
10721
|
+
if (typeof parent !== "object" || parent === null)
|
|
10722
|
+
throw new Error(`patch: missing path ${op.path.join(".")}`);
|
|
10723
|
+
const key = op.path.at(-1);
|
|
10724
|
+
const container = parent;
|
|
10725
|
+
if (op.op === "set")
|
|
10726
|
+
container[key] = clone2(op.value);
|
|
10727
|
+
else if (op.op === "del")
|
|
10728
|
+
delete container[key];
|
|
10729
|
+
else {
|
|
10730
|
+
const array = container[key];
|
|
10731
|
+
if (!Array.isArray(array))
|
|
10732
|
+
throw new Error(`patch: ${op.path.join(".")} is not an array`);
|
|
10733
|
+
if (op.op === "push")
|
|
10734
|
+
array.push(...clone2(op.values));
|
|
10735
|
+
else
|
|
10736
|
+
array.length = op.length;
|
|
10737
|
+
}
|
|
10738
|
+
}
|
|
10739
|
+
return root;
|
|
10740
|
+
}
|
|
10741
|
+
var isPath = (value) => Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number" && Number.isInteger(key) && key >= 0);
|
|
10742
|
+
var isPatch = (value) => Array.isArray(value) && value.every((op) => {
|
|
10743
|
+
if (!isPlainObject2(op) || !isPath(op.path))
|
|
10744
|
+
return false;
|
|
10745
|
+
if (op.op === "set")
|
|
10746
|
+
return "value" in op;
|
|
10747
|
+
if (op.op === "del")
|
|
10748
|
+
return op.path.length > 0;
|
|
10749
|
+
if (op.op === "push")
|
|
10750
|
+
return Array.isArray(op.values);
|
|
10751
|
+
if (op.op === "cut")
|
|
10752
|
+
return typeof op.length === "number" && Number.isInteger(op.length) && op.length >= 0;
|
|
10753
|
+
return false;
|
|
10754
|
+
});
|
|
10755
|
+
|
|
10756
|
+
// packages/workit-core/src/store/reduce.ts
|
|
10757
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
10758
|
+
var OPENING_TYPES = new Set([
|
|
10759
|
+
"task.opened",
|
|
10760
|
+
"task.imported",
|
|
10761
|
+
"migrated.from_v2",
|
|
10762
|
+
"task.checkpoint"
|
|
10763
|
+
]);
|
|
10764
|
+
var KNOWN_TYPES = new Set([
|
|
10765
|
+
...OPENING_TYPES,
|
|
10766
|
+
"task.bound",
|
|
10767
|
+
"task.noted",
|
|
10768
|
+
"task.revised",
|
|
10769
|
+
"task.judged",
|
|
10770
|
+
"task.paused",
|
|
10771
|
+
"task.resumed",
|
|
10772
|
+
"task.closed",
|
|
10773
|
+
"task.updated",
|
|
10774
|
+
"check.ran",
|
|
10775
|
+
"evidence.recorded",
|
|
10776
|
+
"finding.recorded",
|
|
10777
|
+
"finding.updated",
|
|
10778
|
+
"decision.recorded",
|
|
10779
|
+
"decision.updated",
|
|
10780
|
+
"worker.updated"
|
|
10781
|
+
]);
|
|
10782
|
+
var isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10783
|
+
var isKey = (value) => isObject3(value) && typeof value.key === "string" && (value.kind === "branch" || value.kind === "detached" || value.kind === "dir") && (value.branch === null || typeof value.branch === "string");
|
|
10784
|
+
var BLOB_REF = "$blob";
|
|
10785
|
+
var blobRef = (value) => isObject3(value) && Object.keys(value).length === 1 && typeof value[BLOB_REF] === "string" && /^[0-9a-f]{64}$/.test(value[BLOB_REF]) ? value[BLOB_REF] : null;
|
|
10786
|
+
var blobDigest = (value) => createHash5("sha256").update(JSON.stringify(value)).digest("hex");
|
|
10787
|
+
var toBlob = (value, blobs) => {
|
|
10788
|
+
if (!isObject3(value))
|
|
10789
|
+
return value;
|
|
10790
|
+
const digest = blobDigest(value);
|
|
10791
|
+
blobs.write(digest, value);
|
|
10792
|
+
return { [BLOB_REF]: digest };
|
|
10793
|
+
};
|
|
10794
|
+
var encodeRecord = (record, blobs) => Array.isArray(record.candidates) ? { ...record, candidates: record.candidates.map((item) => toBlob(item, blobs)) } : record;
|
|
10795
|
+
var encodeOps = (ops, blobs) => ops.map((op) => {
|
|
10796
|
+
if (op.path[0] !== "candidates")
|
|
10797
|
+
return op;
|
|
10798
|
+
if (op.op === "push" && op.path.length === 1)
|
|
10799
|
+
return { ...op, values: op.values.map((item) => toBlob(item, blobs)) };
|
|
10800
|
+
if (op.op === "set" && op.path.length === 1 && Array.isArray(op.value))
|
|
10801
|
+
return { ...op, value: op.value.map((item) => toBlob(item, blobs)) };
|
|
10802
|
+
if (op.op === "set" && op.path.length === 2)
|
|
10803
|
+
return { ...op, value: toBlob(op.value, blobs) };
|
|
10804
|
+
return op;
|
|
10805
|
+
});
|
|
10806
|
+
var decode2 = (value, blobs) => {
|
|
10807
|
+
const digest = blobRef(value);
|
|
10808
|
+
if (digest)
|
|
10809
|
+
return blobs.read(digest);
|
|
10810
|
+
if (Array.isArray(value))
|
|
10811
|
+
return value.map((item) => decode2(item, blobs));
|
|
10812
|
+
if (isObject3(value)) {
|
|
10813
|
+
const out = {};
|
|
10814
|
+
for (const [key, item] of Object.entries(value))
|
|
10815
|
+
out[key] = decode2(item, blobs);
|
|
10816
|
+
return out;
|
|
10817
|
+
}
|
|
10818
|
+
return value;
|
|
10819
|
+
};
|
|
10820
|
+
function reduce(state, event, blobs) {
|
|
10821
|
+
const data = event.data;
|
|
10822
|
+
const known = KNOWN_TYPES.has(event.type);
|
|
10823
|
+
const carries = "record" in data || "ops" in data || "key" in data;
|
|
10824
|
+
if (!known && !carries) {
|
|
10825
|
+
if (event.critical === true)
|
|
10826
|
+
throw new LogDamage(`event ${event.type} was written by a newer Workit and must be understood; upgrade Workit to read this task`, true);
|
|
10827
|
+
return state;
|
|
10828
|
+
}
|
|
10829
|
+
let next = state;
|
|
10830
|
+
if ("record" in data) {
|
|
10831
|
+
if (!isObject3(data.record))
|
|
10832
|
+
throw new LogDamage(`event ${event.seq} carries a malformed record`);
|
|
10833
|
+
next = {
|
|
10834
|
+
record: decode2(data.record, blobs),
|
|
10835
|
+
key: null,
|
|
10836
|
+
legacy: null
|
|
10837
|
+
};
|
|
10838
|
+
} else if (next === null) {
|
|
10839
|
+
throw new LogDamage(`the log does not start with an opening event (found ${event.type})`);
|
|
10840
|
+
} else
|
|
10841
|
+
next = { ...next };
|
|
10842
|
+
if ("ops" in data) {
|
|
10843
|
+
if (!isPatch(data.ops))
|
|
10844
|
+
throw new LogDamage(`event ${event.seq} carries a malformed patch`);
|
|
10845
|
+
try {
|
|
10846
|
+
next.record = apply(next.record, decode2(data.ops, blobs));
|
|
10847
|
+
} catch (error) {
|
|
10848
|
+
throw new LogDamage(`event ${event.seq} does not apply: ${String(error)}`);
|
|
10849
|
+
}
|
|
10850
|
+
}
|
|
10851
|
+
if ("key" in data)
|
|
10852
|
+
next.key = isKey(data.key) ? data.key : null;
|
|
10853
|
+
if ("legacy" in data && isObject3(data.legacy))
|
|
10854
|
+
next.legacy = {
|
|
10855
|
+
path: String(data.legacy.path ?? ""),
|
|
10856
|
+
digest: String(data.legacy.digest ?? "")
|
|
10857
|
+
};
|
|
10858
|
+
return next;
|
|
10859
|
+
}
|
|
10860
|
+
function eventType(before, after) {
|
|
10861
|
+
const count = (value) => Array.isArray(value) ? value.length : 0;
|
|
10862
|
+
if (before.status !== after.status)
|
|
10863
|
+
return after.status === "closed" ? "task.closed" : after.status === "paused" ? "task.paused" : "task.resumed";
|
|
10864
|
+
if (count(after.evidence) > count(before.evidence)) {
|
|
10865
|
+
const last = after.evidence.at(-1);
|
|
10866
|
+
return last?.data?.observation ? "check.ran" : "evidence.recorded";
|
|
10867
|
+
}
|
|
10868
|
+
if (count(after.findings) > count(before.findings))
|
|
10869
|
+
return "finding.recorded";
|
|
10870
|
+
if (count(after.decisions) > count(before.decisions))
|
|
10871
|
+
return "decision.recorded";
|
|
10872
|
+
if (JSON.stringify(before.findings) !== JSON.stringify(after.findings))
|
|
10873
|
+
return "finding.updated";
|
|
10874
|
+
if (JSON.stringify(before.decisions) !== JSON.stringify(after.decisions))
|
|
10875
|
+
return "decision.updated";
|
|
10876
|
+
if (JSON.stringify(before.workers) !== JSON.stringify(after.workers))
|
|
10877
|
+
return "worker.updated";
|
|
10878
|
+
if (JSON.stringify(before.intent) !== JSON.stringify(after.intent))
|
|
10879
|
+
return "task.revised";
|
|
10880
|
+
if (JSON.stringify(before.assessments) !== JSON.stringify(after.assessments) || JSON.stringify(before.policy) !== JSON.stringify(after.policy))
|
|
10881
|
+
return "task.judged";
|
|
10882
|
+
if (JSON.stringify(before.progress) !== JSON.stringify(after.progress))
|
|
10883
|
+
return "task.noted";
|
|
10884
|
+
return "task.updated";
|
|
10885
|
+
}
|
|
10886
|
+
|
|
10887
|
+
// packages/workit-core/src/store/notes.ts
|
|
10888
|
+
var SLOT = Symbol.for("workit.migrationReporter");
|
|
10889
|
+
var reportMigration = (report) => {
|
|
10890
|
+
try {
|
|
10891
|
+
const reporter = globalThis[SLOT];
|
|
10892
|
+
if (typeof reporter === "function")
|
|
10893
|
+
reporter(report);
|
|
10894
|
+
} catch {}
|
|
10895
|
+
};
|
|
10896
|
+
var migrationReporterInstalled = () => typeof globalThis[SLOT] === "function";
|
|
10897
|
+
|
|
10898
|
+
// packages/workit-core/src/core/task-store.ts
|
|
10899
|
+
var MIGRATION_PENDING = "workit migration pending";
|
|
10900
|
+
var STORE_FORMAT = 3;
|
|
10901
|
+
var STORE_MIN_RUNTIME = "3.0.0";
|
|
10902
|
+
var SNAPSHOT_EVERY = 32;
|
|
10903
|
+
var COMPACT_ABOVE = 200;
|
|
10904
|
+
var COMPACT_KEEP = 50;
|
|
10905
|
+
var AUTO_COMPACT_BYTES = 2 * 1024 * 1024;
|
|
10906
|
+
var TOMBSTONE = Symbol("tombstone");
|
|
10907
|
+
var STALE_TEMPORARY_MS = 60 * 60000;
|
|
10908
|
+
var INDEX_VERSION = 3;
|
|
10909
|
+
var isObject4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10910
|
+
var hostSession = (value) => isObject4(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
10911
|
+
var indexEntry = (task, key, legacy, file) => {
|
|
10912
|
+
const sessions = [];
|
|
10913
|
+
const intent = hostSession(task.intent.provenance.session);
|
|
10914
|
+
if (intent)
|
|
10915
|
+
sessions.push({ ...intent, workerId: null });
|
|
10916
|
+
for (const worker of task.workers) {
|
|
10917
|
+
const session = hostSession(worker.data.session);
|
|
10918
|
+
if (session)
|
|
10919
|
+
sessions.push({ ...session, workerId: worker.id });
|
|
10920
|
+
}
|
|
10921
|
+
return {
|
|
10922
|
+
id: task.id,
|
|
10923
|
+
revision: task.revision,
|
|
10924
|
+
status: task.status,
|
|
10925
|
+
createdAt: task.createdAt,
|
|
10926
|
+
updatedAt: task.updatedAt,
|
|
10927
|
+
objective: task.intent.data.objective,
|
|
10928
|
+
source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
|
|
10929
|
+
progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
|
|
10930
|
+
sessions,
|
|
10931
|
+
workspaceId: task.workspaceId,
|
|
10932
|
+
key: key?.key ?? null,
|
|
10933
|
+
branch: key?.branch ?? null,
|
|
10934
|
+
boundAt: key?.boundAt ?? null,
|
|
10935
|
+
legacy,
|
|
10936
|
+
file
|
|
10937
|
+
};
|
|
10938
|
+
};
|
|
10939
|
+
var fileSignature = (file) => {
|
|
10940
|
+
try {
|
|
10941
|
+
const stat = fs8.statSync(file, { bigint: true });
|
|
10942
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
10943
|
+
} catch {
|
|
10944
|
+
return null;
|
|
10945
|
+
}
|
|
10946
|
+
};
|
|
10947
|
+
var RACY_WINDOW_NS = 2000000000n;
|
|
10948
|
+
var racySignature = (signature) => {
|
|
10949
|
+
const [mtime = 0n, ctime = 0n] = signature.split(":").slice(3, 5).map(BigInt);
|
|
10950
|
+
const changed = mtime > ctime ? mtime : ctime;
|
|
10951
|
+
return BigInt(Date.now()) * 1000000n - changed < RACY_WINDOW_NS;
|
|
10952
|
+
};
|
|
10953
|
+
var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
10954
|
+
var oldestFirst = (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
|
|
10955
|
+
var cachedRuntimeVersion = null;
|
|
10956
|
+
var runtimeVersion = () => {
|
|
10957
|
+
if (cachedRuntimeVersion === null) {
|
|
10958
|
+
try {
|
|
10959
|
+
const pkg = JSON.parse(fs8.readFileSync(path9.join(packageRoot(), "package.json"), "utf8"));
|
|
10960
|
+
cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
10961
|
+
} catch {
|
|
10962
|
+
cachedRuntimeVersion = "0.0.0";
|
|
10963
|
+
}
|
|
10964
|
+
}
|
|
10965
|
+
return cachedRuntimeVersion;
|
|
10966
|
+
};
|
|
10967
|
+
var stampNew = (record) => ({
|
|
10968
|
+
createdWith: record.runtime?.createdWith ?? null,
|
|
10969
|
+
updatedWith: runtimeVersion()
|
|
10970
|
+
});
|
|
10971
|
+
var isNewerVersion = (candidate, current) => {
|
|
10972
|
+
const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
|
|
10973
|
+
const [candidateParts, currentParts] = [parts(candidate), parts(current)];
|
|
10974
|
+
for (let index = 0;index < 3; index += 1) {
|
|
10975
|
+
const left = candidateParts[index] ?? 0;
|
|
10976
|
+
const right = currentParts[index] ?? 0;
|
|
10977
|
+
if (left !== right)
|
|
10978
|
+
return left > right;
|
|
10979
|
+
}
|
|
10980
|
+
return false;
|
|
10981
|
+
};
|
|
10982
|
+
var validId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
10983
|
+
var sameDirectoryIdentity = (left, right) => {
|
|
10984
|
+
if (!path9.isAbsolute(left) || !path9.isAbsolute(right))
|
|
10985
|
+
return false;
|
|
10986
|
+
const normalizedLeft = path9.resolve(left);
|
|
10987
|
+
const normalizedRight = path9.resolve(right);
|
|
10988
|
+
try {
|
|
10989
|
+
const a = fs8.statSync(normalizedLeft, { bigint: true });
|
|
10990
|
+
const b = fs8.statSync(normalizedRight, { bigint: true });
|
|
10991
|
+
if (!a.isDirectory() || !b.isDirectory())
|
|
10992
|
+
return false;
|
|
10993
|
+
if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
|
|
10994
|
+
return true;
|
|
10995
|
+
const canonicalLeft = fs8.realpathSync(normalizedLeft);
|
|
10996
|
+
const canonicalRight = fs8.realpathSync(normalizedRight);
|
|
10997
|
+
if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
|
|
10998
|
+
return true;
|
|
10999
|
+
return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
|
|
11000
|
+
} catch {
|
|
11001
|
+
return false;
|
|
11002
|
+
}
|
|
11003
|
+
};
|
|
10363
11004
|
var externalActionLockRoots = new AsyncLocalStorage;
|
|
10364
11005
|
var heldInProcess = new Map;
|
|
10365
|
-
var
|
|
10366
|
-
var
|
|
10367
|
-
const count = (heldInProcess.get(
|
|
11006
|
+
var hold = (lock) => heldInProcess.set(lock, (heldInProcess.get(lock) ?? 0) + 1);
|
|
11007
|
+
var drop = (lock) => {
|
|
11008
|
+
const count = (heldInProcess.get(lock) ?? 1) - 1;
|
|
10368
11009
|
if (count > 0)
|
|
10369
|
-
heldInProcess.set(
|
|
11010
|
+
heldInProcess.set(lock, count);
|
|
10370
11011
|
else
|
|
10371
|
-
heldInProcess.delete(
|
|
11012
|
+
heldInProcess.delete(lock);
|
|
10372
11013
|
};
|
|
10373
|
-
var
|
|
10374
|
-
const
|
|
10375
|
-
return
|
|
11014
|
+
var envActor = () => {
|
|
11015
|
+
const value = (key) => process.env[key]?.trim() || null;
|
|
11016
|
+
return {
|
|
11017
|
+
host: value("WORKIT_HOST") ?? "runtime",
|
|
11018
|
+
session: value("WORKIT_SESSION_ID"),
|
|
11019
|
+
agentId: value("WORKIT_AGENT_ID")
|
|
11020
|
+
};
|
|
10376
11021
|
};
|
|
10377
11022
|
|
|
10378
11023
|
class TaskStore {
|
|
10379
11024
|
root;
|
|
11025
|
+
origin;
|
|
10380
11026
|
lockTimeoutMs;
|
|
11027
|
+
actor;
|
|
11028
|
+
migrateOnRead;
|
|
11029
|
+
located;
|
|
11030
|
+
migrationChecked = false;
|
|
11031
|
+
compactDue = new Set;
|
|
10381
11032
|
constructor(root, options = {}) {
|
|
10382
|
-
|
|
11033
|
+
const resolved = path9.resolve(root);
|
|
11034
|
+
this.origin = fs8.existsSync(resolved) ? fs8.realpathSync(resolved) : resolved;
|
|
11035
|
+
this.located = resolveStore(this.origin);
|
|
11036
|
+
this.root = checkoutRootOf(this.origin, this.located);
|
|
10383
11037
|
this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
|
|
11038
|
+
this.actor = options.actor ?? envActor();
|
|
11039
|
+
this.migrateOnRead = options.migrateOnRead;
|
|
11040
|
+
}
|
|
11041
|
+
location() {
|
|
11042
|
+
if (this.located !== undefined && !(this.located instanceof Error) && !this.located.shared && fs8.existsSync(path9.join(this.root, ".git"))) {
|
|
11043
|
+
this.located = undefined;
|
|
11044
|
+
this.migrationChecked = false;
|
|
11045
|
+
}
|
|
11046
|
+
if (this.located === undefined)
|
|
11047
|
+
this.located = resolveStore(this.root);
|
|
11048
|
+
return this.located instanceof Error ? failure2("storage_error", this.located.message, { path: this.root }) : success(null, null, this.located);
|
|
11049
|
+
}
|
|
11050
|
+
currentKey() {
|
|
11051
|
+
const location = this.location();
|
|
11052
|
+
if (!location.ok)
|
|
11053
|
+
return location;
|
|
11054
|
+
return success(null, null, resolveTaskKey(this.root, location.data));
|
|
10384
11055
|
}
|
|
10385
11056
|
readTask(taskId) {
|
|
10386
11057
|
if (!validId(taskId))
|
|
10387
11058
|
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
10388
|
-
const
|
|
10389
|
-
if (!
|
|
11059
|
+
const ready = this.ready();
|
|
11060
|
+
if (!ready.ok)
|
|
11061
|
+
return ready;
|
|
11062
|
+
const loaded = this.load(taskId);
|
|
11063
|
+
if (!loaded.ok)
|
|
11064
|
+
return loaded;
|
|
11065
|
+
if (!loaded.data)
|
|
10390
11066
|
return failure2("not_found", "task not found", { taskId });
|
|
10391
|
-
if (result.result.ok && result.result.data.id !== taskId)
|
|
10392
|
-
return failure2("recovery_required", "task filename and record ID differ", { taskId });
|
|
10393
|
-
if (result.result.ok) {
|
|
10394
|
-
const workspace = this.readWorkspace();
|
|
10395
|
-
if (!workspace.ok)
|
|
10396
|
-
return workspace;
|
|
10397
|
-
if (!workspace.data || result.result.data.workspaceId !== workspace.data.id)
|
|
10398
|
-
return failure2("recovery_required", "task workspace binding is invalid", { taskId });
|
|
10399
|
-
}
|
|
10400
|
-
return result.result;
|
|
10401
|
-
}
|
|
10402
|
-
listTasks() {
|
|
10403
|
-
if (!fs6.existsSync(this.tasksDir))
|
|
10404
|
-
return success(null, null, []);
|
|
10405
|
-
let names;
|
|
10406
|
-
try {
|
|
10407
|
-
names = fs6.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
|
|
10408
|
-
} catch (error) {
|
|
10409
|
-
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
10410
|
-
path: this.tasksDir
|
|
10411
|
-
});
|
|
10412
|
-
}
|
|
10413
11067
|
const workspace = this.readWorkspace();
|
|
10414
11068
|
if (!workspace.ok)
|
|
10415
11069
|
return workspace;
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
11070
|
+
const bound = this.bindingError(loaded.data.record, workspace.data);
|
|
11071
|
+
return bound ?? success(null, null, loaded.data.record);
|
|
11072
|
+
}
|
|
11073
|
+
listTasks() {
|
|
11074
|
+
const listed = this.listTaskIndex();
|
|
11075
|
+
if (!listed.ok)
|
|
11076
|
+
return listed;
|
|
10420
11077
|
const tasks = [];
|
|
10421
|
-
for (const
|
|
10422
|
-
const
|
|
10423
|
-
if (!
|
|
10424
|
-
|
|
10425
|
-
if (
|
|
10426
|
-
|
|
10427
|
-
if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
|
|
10428
|
-
return failure2("recovery_required", "task filename and record ID differ", { path: name });
|
|
10429
|
-
if (item.result.data.workspaceId !== workspace.data.id)
|
|
10430
|
-
return failure2("recovery_required", "task workspace binding is invalid", { path: name });
|
|
10431
|
-
tasks.push(item.result.data);
|
|
11078
|
+
for (const entry of listed.data.toSorted((left, right) => left.id.localeCompare(right.id))) {
|
|
11079
|
+
const loaded = this.load(entry.id);
|
|
11080
|
+
if (!loaded.ok)
|
|
11081
|
+
return loaded;
|
|
11082
|
+
if (loaded.data)
|
|
11083
|
+
tasks.push(loaded.data.record);
|
|
10432
11084
|
}
|
|
10433
11085
|
return success(null, null, tasks);
|
|
10434
11086
|
}
|
|
10435
11087
|
listTaskIndex() {
|
|
10436
|
-
|
|
11088
|
+
const all = this.listStoreIndex();
|
|
11089
|
+
if (!all.ok)
|
|
11090
|
+
return all;
|
|
11091
|
+
const workspace = this.readWorkspace();
|
|
11092
|
+
if (!workspace.ok)
|
|
11093
|
+
return workspace;
|
|
11094
|
+
if (!workspace.data)
|
|
10437
11095
|
return success(null, null, []);
|
|
11096
|
+
const id = workspace.data.id;
|
|
11097
|
+
return success(null, null, all.data.filter((entry) => entry.workspaceId === id));
|
|
11098
|
+
}
|
|
11099
|
+
listStoreIndex() {
|
|
11100
|
+
const ready = this.ready();
|
|
11101
|
+
if (!ready.ok)
|
|
11102
|
+
return ready;
|
|
11103
|
+
const tasksDir = this.paths().tasks;
|
|
10438
11104
|
let names;
|
|
10439
11105
|
try {
|
|
10440
|
-
names =
|
|
11106
|
+
names = fs8.existsSync(tasksDir) ? fs8.readdirSync(tasksDir).filter((name) => validId(name)) : [];
|
|
10441
11107
|
} catch (error) {
|
|
10442
11108
|
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
10443
|
-
path:
|
|
11109
|
+
path: tasksDir
|
|
10444
11110
|
});
|
|
10445
11111
|
}
|
|
10446
|
-
const
|
|
10447
|
-
if (!workspace.ok)
|
|
10448
|
-
return workspace;
|
|
10449
|
-
if (!workspace.data)
|
|
10450
|
-
return failure2("recovery_required", "task workspace binding is invalid", {
|
|
10451
|
-
path: this.workspacePath
|
|
10452
|
-
});
|
|
10453
|
-
const stored = this.readIndex(workspace.data.id);
|
|
11112
|
+
const stored = this.readIndex();
|
|
10454
11113
|
const entries = [];
|
|
10455
11114
|
let changed = Object.keys(stored).length !== names.length;
|
|
10456
11115
|
for (const name of names.toSorted()) {
|
|
10457
|
-
const
|
|
10458
|
-
const signature = fileSignature(file);
|
|
11116
|
+
const signature = fileSignature(this.eventsPath(name));
|
|
10459
11117
|
if (signature === null) {
|
|
10460
11118
|
changed = true;
|
|
10461
11119
|
continue;
|
|
10462
11120
|
}
|
|
10463
|
-
const cached = stored[name
|
|
11121
|
+
const cached = stored[name];
|
|
10464
11122
|
if (cached && cached.file === signature && !racySignature(signature)) {
|
|
10465
11123
|
entries.push(cached);
|
|
10466
11124
|
continue;
|
|
10467
11125
|
}
|
|
10468
|
-
const
|
|
10469
|
-
if (!
|
|
11126
|
+
const loaded = this.load(name);
|
|
11127
|
+
if (!loaded.ok)
|
|
11128
|
+
return loaded;
|
|
11129
|
+
if (!loaded.data)
|
|
10470
11130
|
continue;
|
|
10471
|
-
|
|
10472
|
-
return item.result;
|
|
10473
|
-
if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
|
|
10474
|
-
return failure2("recovery_required", "task filename and record ID differ", { path: name });
|
|
10475
|
-
if (item.result.data.workspaceId !== workspace.data.id)
|
|
10476
|
-
return failure2("recovery_required", "task workspace binding is invalid", { path: name });
|
|
10477
|
-
const entry = indexEntry(item.result.data, signature);
|
|
11131
|
+
const entry = indexEntry(loaded.data.record, loaded.data.key, loaded.data.legacy !== null, signature);
|
|
10478
11132
|
if (!cached || canonicalJson(cached) !== canonicalJson(entry))
|
|
10479
11133
|
changed = true;
|
|
10480
11134
|
entries.push(entry);
|
|
10481
11135
|
}
|
|
10482
11136
|
if (changed)
|
|
10483
|
-
this.writeIndex(
|
|
11137
|
+
this.writeIndex(entries);
|
|
10484
11138
|
return success(null, null, entries);
|
|
10485
11139
|
}
|
|
10486
11140
|
readWorkspace() {
|
|
10487
|
-
const
|
|
10488
|
-
if (!
|
|
10489
|
-
return
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
11141
|
+
const ready = this.ready();
|
|
11142
|
+
if (!ready.ok)
|
|
11143
|
+
return ready;
|
|
11144
|
+
const file = this.paths().workspace;
|
|
11145
|
+
let bytes;
|
|
11146
|
+
try {
|
|
11147
|
+
bytes = retryTransient(() => fs8.readFileSync(file, "utf8"));
|
|
11148
|
+
} catch (error) {
|
|
11149
|
+
if (error.code === "ENOENT")
|
|
11150
|
+
return success(null, null, null);
|
|
11151
|
+
return failure2("recovery_required", `workspace record cannot be read: ${String(error)}`, {
|
|
11152
|
+
path: file
|
|
10493
11153
|
});
|
|
10494
|
-
|
|
11154
|
+
}
|
|
11155
|
+
const parsed = this.parseRecord(bytes, workspaceRecordSchema, "workspace");
|
|
11156
|
+
if (!parsed.ok)
|
|
11157
|
+
return parsed;
|
|
11158
|
+
if (parsed.data.root !== this.root && !sameDirectoryIdentity(parsed.data.root, this.root))
|
|
11159
|
+
return failure2("recovery_required", "workspace root binding is invalid", { path: file });
|
|
11160
|
+
return parsed;
|
|
11161
|
+
}
|
|
11162
|
+
implicitTask(input) {
|
|
11163
|
+
const ready = this.ready(input.create !== false);
|
|
11164
|
+
if (!ready.ok)
|
|
11165
|
+
return ready;
|
|
11166
|
+
const key = this.currentKey();
|
|
11167
|
+
if (!key.ok)
|
|
11168
|
+
return key;
|
|
11169
|
+
const found = this.openTaskForKey(key.data.key);
|
|
11170
|
+
if (!found.ok)
|
|
11171
|
+
return found;
|
|
11172
|
+
const workspace = this.readWorkspace();
|
|
11173
|
+
if (!workspace.ok)
|
|
11174
|
+
return workspace;
|
|
11175
|
+
if (found.data && workspace.data && found.data.workspaceId === workspace.data.id && (found.data.key === key.data.key || input.create === false)) {
|
|
11176
|
+
const task = this.readTask(found.data.id);
|
|
11177
|
+
return task.ok ? success(task.revision, null, { task: task.data, created: false, key: key.data }) : task;
|
|
11178
|
+
}
|
|
11179
|
+
if (input.create === false) {
|
|
11180
|
+
if (!found.data)
|
|
11181
|
+
return success(null, null, null);
|
|
11182
|
+
const loaded = this.load(found.data.id);
|
|
11183
|
+
if (!loaded.ok)
|
|
11184
|
+
return loaded;
|
|
11185
|
+
return loaded.data ? success(null, null, { task: loaded.data.record, created: false, key: key.data }) : success(null, null, null);
|
|
11186
|
+
}
|
|
11187
|
+
return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => this.implicitLocked(input, key.data)));
|
|
11188
|
+
}
|
|
11189
|
+
implicitLocked(input, key) {
|
|
11190
|
+
{
|
|
11191
|
+
const again = this.openTaskForKey(key.key);
|
|
11192
|
+
if (!again.ok)
|
|
11193
|
+
return again;
|
|
11194
|
+
if (again.data) {
|
|
11195
|
+
const moved = this.bindLocked(again.data.id, key, input.now);
|
|
11196
|
+
return moved.ok ? success(moved.data.revision, null, { task: moved.data, created: false, key }) : moved;
|
|
11197
|
+
}
|
|
11198
|
+
const label = key.kind === "branch" ? `branch ${key.branch}` : key.kind === "detached" ? "a detached HEAD" : path9.basename(this.root) || this.root;
|
|
11199
|
+
const intent = {
|
|
11200
|
+
objective: input.objective?.trim() || `Work on ${label}`,
|
|
11201
|
+
scope: { description: `the checkout (${label})`, paths: ["."], exclusions: [] },
|
|
11202
|
+
authorityRefs: []
|
|
11203
|
+
};
|
|
11204
|
+
const created = this.createLocked({ intent, provenance: input.provenance, expectedWorkspaceRevision: null, now: input.now }, false, key, false);
|
|
11205
|
+
return created.ok ? success(created.data.revision, null, { task: created.data, created: true, key }) : created;
|
|
11206
|
+
}
|
|
11207
|
+
}
|
|
11208
|
+
adoptTask(taskId) {
|
|
11209
|
+
if (!validId(taskId))
|
|
11210
|
+
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
11211
|
+
const ready = this.ready(true);
|
|
11212
|
+
if (!ready.ok)
|
|
11213
|
+
return ready;
|
|
11214
|
+
const key = this.currentKey();
|
|
11215
|
+
if (!key.ok)
|
|
11216
|
+
return key;
|
|
11217
|
+
return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => {
|
|
11218
|
+
const holder = this.openTaskForKey(key.data.key);
|
|
11219
|
+
if (!holder.ok)
|
|
11220
|
+
return holder;
|
|
11221
|
+
if (holder.data && holder.data.id !== taskId)
|
|
11222
|
+
return failure2("invalid_transition", `${key.data.key} already has open task ${holder.data.id}; close it (\`workit task close\`) before adopting another`, { taskId: holder.data.id });
|
|
11223
|
+
return this.bindLocked(taskId, key.data);
|
|
11224
|
+
}));
|
|
11225
|
+
}
|
|
11226
|
+
keyReport() {
|
|
11227
|
+
const key = this.currentKey();
|
|
11228
|
+
if (!key.ok)
|
|
11229
|
+
return key;
|
|
11230
|
+
const bound = this.openTasksForKey(key.data.key);
|
|
11231
|
+
if (!bound.ok)
|
|
11232
|
+
return bound;
|
|
11233
|
+
return success(null, null, { key: key.data, bound: bound.data });
|
|
10495
11234
|
}
|
|
10496
11235
|
create(input, provenance, expectedWorkspaceRevision) {
|
|
10497
11236
|
const value = "intent" in input ? input : {
|
|
@@ -10499,82 +11238,16 @@ class TaskStore {
|
|
|
10499
11238
|
provenance,
|
|
10500
11239
|
expectedWorkspaceRevision: expectedWorkspaceRevision ?? null
|
|
10501
11240
|
};
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
|
|
10507
|
-
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
10508
|
-
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
10509
|
-
actualWorkspaceRevision: current.data?.revision ?? null
|
|
10510
|
-
});
|
|
10511
|
-
}
|
|
10512
|
-
if (!value.provenance)
|
|
10513
|
-
return failure2("invalid_input", "provenance is required");
|
|
10514
|
-
if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
|
|
10515
|
-
return failure2("invalid_input", "task intent or provenance is invalid");
|
|
10516
|
-
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
10517
|
-
if (current.data && !previousWorkspaceBytes)
|
|
10518
|
-
return failure2("storage_error", "workspace snapshot disappeared during creation");
|
|
10519
|
-
const workspace = current.data ? {
|
|
10520
|
-
...current.data,
|
|
10521
|
-
revision: newRevision(),
|
|
10522
|
-
root: this.root,
|
|
10523
|
-
runtime: stampNew(current.data)
|
|
10524
|
-
} : {
|
|
10525
|
-
schemaVersion: SCHEMA_VERSION,
|
|
10526
|
-
id: newId(),
|
|
10527
|
-
revision: newRevision(),
|
|
10528
|
-
root: this.root,
|
|
10529
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
10530
|
-
writer: null
|
|
10531
|
-
};
|
|
10532
|
-
const timestamp = value.now ?? now();
|
|
10533
|
-
const task = {
|
|
10534
|
-
schemaVersion: SCHEMA_VERSION,
|
|
10535
|
-
id: newId(),
|
|
10536
|
-
workspaceId: workspace.id,
|
|
10537
|
-
revision: newRevision(),
|
|
10538
|
-
createdAt: timestamp,
|
|
10539
|
-
updatedAt: timestamp,
|
|
10540
|
-
origin: null,
|
|
10541
|
-
intent: {
|
|
10542
|
-
id: newId(),
|
|
10543
|
-
recordedAt: timestamp,
|
|
10544
|
-
provenance: value.provenance,
|
|
10545
|
-
data: value.intent
|
|
10546
|
-
},
|
|
10547
|
-
constraints: [],
|
|
10548
|
-
status: "active",
|
|
10549
|
-
closure: null,
|
|
10550
|
-
progress: { summary: "", nextAction: null, blockers: [] },
|
|
10551
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
10552
|
-
assessments: [],
|
|
10553
|
-
policy: null,
|
|
10554
|
-
policyChanges: [],
|
|
10555
|
-
candidates: [],
|
|
10556
|
-
evidence: [],
|
|
10557
|
-
decisions: [],
|
|
10558
|
-
findings: [],
|
|
10559
|
-
workers: []
|
|
10560
|
-
};
|
|
10561
|
-
const writtenWorkspace = this.replaceSnapshot(this.workspacePath, workspace, previousWorkspaceBytes);
|
|
10562
|
-
if (!writtenWorkspace.ok)
|
|
10563
|
-
return writtenWorkspace;
|
|
10564
|
-
const writtenTask = this.replaceSnapshot(this.taskPath(task.id), task, null);
|
|
10565
|
-
if (!writtenTask.ok) {
|
|
10566
|
-
if (current.data && previousWorkspaceBytes) {
|
|
10567
|
-
const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
|
|
10568
|
-
if (!restored.ok)
|
|
10569
|
-
return failure2("external_outcome_unknown", "task creation failed and workspace restoration is uncertain", { operation: "create", outcome: "unknown" });
|
|
10570
|
-
}
|
|
10571
|
-
return failure2("external_outcome_unknown", "workspace created but task write is uncertain", { operation: "create", outcome: "unknown" });
|
|
10572
|
-
}
|
|
10573
|
-
return success(task.revision, workspace.revision, task);
|
|
10574
|
-
});
|
|
11241
|
+
const ready = this.ready(true);
|
|
11242
|
+
if (!ready.ok)
|
|
11243
|
+
return ready;
|
|
11244
|
+
return this.withLock(this.paths().checkoutLock, () => this.createLocked(value, true, null));
|
|
10575
11245
|
}
|
|
10576
11246
|
importTask(input) {
|
|
10577
|
-
|
|
11247
|
+
const ready = this.ready(true);
|
|
11248
|
+
if (!ready.ok)
|
|
11249
|
+
return ready;
|
|
11250
|
+
return this.withLock(this.paths().checkoutLock, () => {
|
|
10578
11251
|
const current = this.readWorkspace();
|
|
10579
11252
|
if (!current.ok)
|
|
10580
11253
|
return current;
|
|
@@ -10606,23 +11279,8 @@ class TaskStore {
|
|
|
10606
11279
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
10607
11280
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
10608
11281
|
});
|
|
10609
|
-
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
10610
|
-
if (current.data && !previousWorkspaceBytes)
|
|
10611
|
-
return failure2("storage_error", "workspace snapshot disappeared during import");
|
|
10612
11282
|
const timestamp = input.now ?? now();
|
|
10613
|
-
const workspace = current.data
|
|
10614
|
-
...current.data,
|
|
10615
|
-
revision: newRevision(),
|
|
10616
|
-
root: this.root,
|
|
10617
|
-
runtime: stampNew(current.data)
|
|
10618
|
-
} : {
|
|
10619
|
-
schemaVersion: SCHEMA_VERSION,
|
|
10620
|
-
id: input.workspaceId ?? newId(),
|
|
10621
|
-
revision: newRevision(),
|
|
10622
|
-
root: this.root,
|
|
10623
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
10624
|
-
writer: null
|
|
10625
|
-
};
|
|
11283
|
+
const workspace = this.nextWorkspace(current.data, input.workspaceId);
|
|
10626
11284
|
const task = {
|
|
10627
11285
|
...input.task,
|
|
10628
11286
|
workspaceId: workspace.id,
|
|
@@ -10637,62 +11295,46 @@ class TaskStore {
|
|
|
10637
11295
|
const validTask = taskRecordSchema.safeParse(task);
|
|
10638
11296
|
if (!validTask.success)
|
|
10639
11297
|
return failure2("invalid_input", "imported task does not satisfy its schema");
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
const writtenTask = this.replaceSnapshot(this.taskPath(task.id), validTask.data, null);
|
|
10644
|
-
if (!writtenTask.ok) {
|
|
10645
|
-
if (current.data && previousWorkspaceBytes) {
|
|
10646
|
-
const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
|
|
10647
|
-
if (!restored.ok)
|
|
10648
|
-
return failure2("external_outcome_unknown", "import failed and workspace restoration is uncertain", { operation: "import", outcome: "unknown" });
|
|
10649
|
-
}
|
|
10650
|
-
return failure2("external_outcome_unknown", `workspace created but task import is uncertain: ${writtenTask.error}`, {
|
|
10651
|
-
operation: "import",
|
|
10652
|
-
outcome: "unknown",
|
|
10653
|
-
path: writtenTask.details.path
|
|
10654
|
-
});
|
|
10655
|
-
}
|
|
10656
|
-
return success(validTask.data.revision, workspace.revision, validTask.data);
|
|
11298
|
+
return this.writeNewTask(current.data, workspace, validTask.data, "task.imported", null, {
|
|
11299
|
+
operation: "import"
|
|
11300
|
+
});
|
|
10657
11301
|
});
|
|
10658
11302
|
}
|
|
10659
11303
|
mutateTask(taskId, expected, update, timestamp) {
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
const
|
|
10667
|
-
if (!
|
|
10668
|
-
return
|
|
11304
|
+
if (!validId(taskId))
|
|
11305
|
+
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
11306
|
+
const ready = this.ready(true);
|
|
11307
|
+
if (!ready.ok)
|
|
11308
|
+
return ready;
|
|
11309
|
+
const result = this.withLock(this.taskLockPath(taskId), () => {
|
|
11310
|
+
const loaded = this.loadBound(taskId);
|
|
11311
|
+
if (!loaded.ok)
|
|
11312
|
+
return loaded;
|
|
11313
|
+
if (loaded.data.record.revision !== expected)
|
|
11314
|
+
return this.conflict(expected, loaded.data.record.revision);
|
|
10669
11315
|
const context = { now: timestamp ?? now(), revision: newRevision() };
|
|
10670
11316
|
let changed;
|
|
10671
11317
|
try {
|
|
10672
|
-
changed = update(
|
|
11318
|
+
changed = update(structuredClone(loaded.data.record), Object.freeze({ ...context }));
|
|
10673
11319
|
} catch (error) {
|
|
10674
11320
|
return failure2("storage_error", `task mutation failed: ${String(error)}`);
|
|
10675
11321
|
}
|
|
10676
11322
|
if (!changed.ok)
|
|
10677
11323
|
return changed;
|
|
10678
|
-
const
|
|
10679
|
-
|
|
10680
|
-
id: taskId,
|
|
10681
|
-
workspaceId: current.data.workspaceId,
|
|
10682
|
-
createdAt: current.data.createdAt,
|
|
10683
|
-
revision: context.revision,
|
|
10684
|
-
updatedAt: context.now,
|
|
10685
|
-
runtime: stampNew(current.data)
|
|
10686
|
-
};
|
|
10687
|
-
const valid = taskRecordSchema.safeParse(record);
|
|
10688
|
-
if (!valid.success)
|
|
11324
|
+
const valid = this.nextTask(loaded.data.record, changed.data, context);
|
|
11325
|
+
if (!valid)
|
|
10689
11326
|
return failure2("invalid_input", "task mutation produced an invalid record");
|
|
10690
|
-
const written = this.
|
|
10691
|
-
return written.ok ? success(valid.
|
|
11327
|
+
const written = this.appendPatch(loaded.data, valid);
|
|
11328
|
+
return written.ok ? success(valid.revision, null, valid) : written;
|
|
10692
11329
|
});
|
|
11330
|
+
this.compactPending();
|
|
11331
|
+
return result;
|
|
10693
11332
|
}
|
|
10694
11333
|
mutateWorkspace(expected, update) {
|
|
10695
|
-
|
|
11334
|
+
const ready = this.ready(true);
|
|
11335
|
+
if (!ready.ok)
|
|
11336
|
+
return ready;
|
|
11337
|
+
return this.withLock(this.paths().checkoutLock, () => {
|
|
10696
11338
|
const current = this.readWorkspace();
|
|
10697
11339
|
if (!current.ok)
|
|
10698
11340
|
return current;
|
|
@@ -10700,9 +11342,6 @@ class TaskStore {
|
|
|
10700
11342
|
return failure2("not_found", "workspace not found");
|
|
10701
11343
|
if (current.data.revision !== expected)
|
|
10702
11344
|
return this.workspaceConflict(expected, current.data.revision);
|
|
10703
|
-
const previousBytes = this.snapshotBytes(this.workspacePath);
|
|
10704
|
-
if (!previousBytes)
|
|
10705
|
-
return failure2("storage_error", "workspace snapshot disappeared during mutation");
|
|
10706
11345
|
const context = { now: now(), revision: newRevision() };
|
|
10707
11346
|
let changed;
|
|
10708
11347
|
try {
|
|
@@ -10712,23 +11351,27 @@ class TaskStore {
|
|
|
10712
11351
|
}
|
|
10713
11352
|
if (!changed.ok)
|
|
10714
11353
|
return changed;
|
|
10715
|
-
const
|
|
11354
|
+
const valid = workspaceRecordSchema.safeParse({
|
|
10716
11355
|
...changed.data,
|
|
10717
11356
|
id: current.data.id,
|
|
10718
11357
|
root: this.root,
|
|
10719
11358
|
revision: context.revision,
|
|
10720
11359
|
runtime: stampNew(current.data)
|
|
10721
|
-
};
|
|
10722
|
-
const valid = workspaceRecordSchema.safeParse(record);
|
|
11360
|
+
});
|
|
10723
11361
|
if (!valid.success)
|
|
10724
11362
|
return failure2("invalid_input", "workspace mutation produced an invalid record");
|
|
10725
|
-
const written = this.
|
|
11363
|
+
const written = this.writeWorkspace(valid.data);
|
|
10726
11364
|
return written.ok ? success(valid.data.revision, valid.data.revision, valid.data) : written;
|
|
10727
11365
|
});
|
|
10728
11366
|
}
|
|
10729
11367
|
mutateTaskAndWorkspace(input) {
|
|
10730
|
-
|
|
10731
|
-
|
|
11368
|
+
if (!validId(input.taskId))
|
|
11369
|
+
return failure2("invalid_input", "task ID is invalid", { taskId: input.taskId });
|
|
11370
|
+
const ready = this.ready(true);
|
|
11371
|
+
if (!ready.ok)
|
|
11372
|
+
return ready;
|
|
11373
|
+
const result = this.withLock(this.paths().checkoutLock, () => this.withLock(this.taskLockPath(input.taskId), () => {
|
|
11374
|
+
const task = this.loadBound(input.taskId);
|
|
10732
11375
|
if (!task.ok)
|
|
10733
11376
|
return task;
|
|
10734
11377
|
const workspace = this.readWorkspace();
|
|
@@ -10739,14 +11382,10 @@ class TaskStore {
|
|
|
10739
11382
|
const expected = input.expectedRevision;
|
|
10740
11383
|
if (!expected)
|
|
10741
11384
|
return failure2("invalid_input", "task revision is required");
|
|
10742
|
-
if (task.data.revision !== expected)
|
|
10743
|
-
return this.conflict(expected, task.data.revision);
|
|
11385
|
+
if (task.data.record.revision !== expected)
|
|
11386
|
+
return this.conflict(expected, task.data.record.revision);
|
|
10744
11387
|
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
10745
11388
|
return this.workspaceConflict(input.expectedWorkspaceRevision, workspace.data.revision);
|
|
10746
|
-
const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
|
|
10747
|
-
const previousTaskBytes = this.snapshotBytes(this.taskPath(input.taskId));
|
|
10748
|
-
if (!previousWorkspaceBytes || !previousTaskBytes)
|
|
10749
|
-
return failure2("storage_error", "snapshot disappeared during coupled mutation");
|
|
10750
11389
|
const workspaceContext = { now: input.now ?? now(), revision: newRevision() };
|
|
10751
11390
|
let changedWorkspace;
|
|
10752
11391
|
try {
|
|
@@ -10765,286 +11404,69 @@ class TaskStore {
|
|
|
10765
11404
|
});
|
|
10766
11405
|
if (!nextWorkspace.success)
|
|
10767
11406
|
return failure2("invalid_input", "workspace mutation produced an invalid record");
|
|
10768
|
-
const reserved = this.
|
|
11407
|
+
const reserved = this.writeWorkspace(nextWorkspace.data);
|
|
10769
11408
|
if (!reserved.ok)
|
|
10770
11409
|
return reserved;
|
|
11410
|
+
const uncertain = (message) => {
|
|
11411
|
+
this.markUncertain(nextWorkspace.data);
|
|
11412
|
+
return failure2("external_outcome_unknown", message, {
|
|
11413
|
+
operation: "coupled_mutation",
|
|
11414
|
+
outcome: "unknown"
|
|
11415
|
+
});
|
|
11416
|
+
};
|
|
10771
11417
|
const taskContext = { now: input.now ?? now(), revision: newRevision() };
|
|
10772
11418
|
let changedTask;
|
|
10773
11419
|
try {
|
|
10774
|
-
changedTask = input.task(task.data, Object.freeze({ ...taskContext }));
|
|
11420
|
+
changedTask = input.task(structuredClone(task.data.record), Object.freeze({ ...taskContext }));
|
|
10775
11421
|
} catch (error) {
|
|
10776
|
-
|
|
10777
|
-
return failure2("external_outcome_unknown", `workspace reserved but task mutation threw: ${String(error)}`, { operation: "coupled_mutation", outcome: "unknown" });
|
|
10778
|
-
}
|
|
10779
|
-
if (!changedTask.ok) {
|
|
10780
|
-
this.markUncertain(nextWorkspace.data);
|
|
10781
|
-
return failure2("external_outcome_unknown", "workspace reserved but task update is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
|
|
11422
|
+
return uncertain(`workspace reserved but task mutation threw: ${String(error)}`);
|
|
10782
11423
|
}
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
11424
|
+
if (!changedTask.ok)
|
|
11425
|
+
return uncertain("workspace reserved but task update is uncertain");
|
|
11426
|
+
const nextTask = this.nextTask(task.data.record, changedTask.data, taskContext);
|
|
11427
|
+
if (!nextTask)
|
|
11428
|
+
return uncertain("workspace reserved but task update is uncertain");
|
|
11429
|
+
const written = this.appendPatch(task.data, nextTask);
|
|
11430
|
+
if (!written.ok)
|
|
11431
|
+
return uncertain("workspace reserved but task append is uncertain");
|
|
11432
|
+
return success(nextTask.revision, nextWorkspace.data.revision, {
|
|
11433
|
+
task: nextTask,
|
|
11434
|
+
workspace: nextWorkspace.data
|
|
10791
11435
|
});
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
}
|
|
10796
|
-
const written = this.replaceSnapshot(this.taskPath(input.taskId), nextTask.data, previousTaskBytes);
|
|
10797
|
-
if (!written.ok) {
|
|
10798
|
-
this.markUncertain(nextWorkspace.data);
|
|
10799
|
-
return failure2("external_outcome_unknown", "workspace reserved but task replacement is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
|
|
10800
|
-
}
|
|
10801
|
-
return success(nextTask.data.revision, nextWorkspace.data.revision, {
|
|
10802
|
-
task: nextTask.data,
|
|
10803
|
-
workspace: nextWorkspace.data
|
|
10804
|
-
});
|
|
10805
|
-
});
|
|
10806
|
-
}
|
|
10807
|
-
recoveryCandidates() {
|
|
10808
|
-
if (!fs6.existsSync(this.recoveryDir))
|
|
10809
|
-
return success(null, null, []);
|
|
10810
|
-
try {
|
|
10811
|
-
const candidates = [];
|
|
10812
|
-
for (const name of fs6.readdirSync(this.recoveryDir)) {
|
|
10813
|
-
const match = RECOVERY_NAME.exec(name);
|
|
10814
|
-
if (match)
|
|
10815
|
-
candidates.push({
|
|
10816
|
-
target: match[1],
|
|
10817
|
-
path: path7.join(this.recoveryDir, name),
|
|
10818
|
-
digest: match[3]
|
|
10819
|
-
});
|
|
10820
|
-
}
|
|
10821
|
-
return success(null, null, candidates);
|
|
10822
|
-
} catch (error) {
|
|
10823
|
-
return failure2("storage_error", `unable to list recovery: ${String(error)}`, {
|
|
10824
|
-
path: this.recoveryDir
|
|
10825
|
-
});
|
|
10826
|
-
}
|
|
10827
|
-
}
|
|
10828
|
-
recoverTask(taskId, input) {
|
|
10829
|
-
return this.recover("task", taskId, input);
|
|
10830
|
-
}
|
|
10831
|
-
recoverWorkspace(input) {
|
|
10832
|
-
return this.recover("workspace", null, input);
|
|
10833
|
-
}
|
|
10834
|
-
recover(target, taskId, input) {
|
|
10835
|
-
if (taskId !== null && !validId(taskId))
|
|
10836
|
-
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
10837
|
-
if (!input.reason || !Array.isArray(input.authorityRefs) || typeof input.processEvidence !== "function" || !validDigest(input.expectedBytes) || !validDigest(input.snapshotDigest))
|
|
10838
|
-
return failure2("invalid_input", "recovery authority is invalid");
|
|
10839
|
-
if (input.authorityRefs.some((ref) => !refSchema.safeParse(ref).success))
|
|
10840
|
-
return failure2("invalid_input", "recovery references are invalid");
|
|
10841
|
-
try {
|
|
10842
|
-
const workspace = this.readWorkspace();
|
|
10843
|
-
if (!workspace.ok && target === "task")
|
|
10844
|
-
return workspace;
|
|
10845
|
-
const workspaceValue = workspace.ok ? workspace.data : null;
|
|
10846
|
-
if (workspaceValue && workspaceValue.revision !== input.expectedWorkspaceRevision)
|
|
10847
|
-
return this.conflict(input.expectedWorkspaceRevision, workspaceValue.revision);
|
|
10848
|
-
const file = target === "workspace" ? this.workspacePath : this.taskPath(taskId);
|
|
10849
|
-
const currentBytes = this.snapshotBytes(file);
|
|
10850
|
-
if (!currentBytes)
|
|
10851
|
-
return failure2("not_found", "snapshot not found");
|
|
10852
|
-
if (digestBytes(currentBytes) !== input.expectedBytes)
|
|
10853
|
-
return failure2("revision_conflict", "snapshot bytes do not match expected bytes");
|
|
10854
|
-
let selectedBytes = currentBytes;
|
|
10855
|
-
if (digestBytes(currentBytes) !== input.snapshotDigest) {
|
|
10856
|
-
const candidate = this.findRecovery(target, taskId, input.snapshotDigest, workspaceValue);
|
|
10857
|
-
if (!candidate)
|
|
10858
|
-
return failure2("recovery_required", "validated recovery snapshot not found");
|
|
10859
|
-
selectedBytes = candidate;
|
|
10860
|
-
}
|
|
10861
|
-
const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
10862
|
-
if (!selectedRecord.ok)
|
|
10863
|
-
return selectedRecord;
|
|
10864
|
-
if (target === "workspace") {
|
|
10865
|
-
const parsed = selectedRecord;
|
|
10866
|
-
if (!parsed.ok || parsed.data.root !== this.root)
|
|
10867
|
-
return failure2("recovery_required", "workspace recovery binding is invalid");
|
|
10868
|
-
} else {
|
|
10869
|
-
const parsed = selectedRecord;
|
|
10870
|
-
if (!parsed.ok || parsed.data.id !== taskId || !workspaceValue || parsed.data.workspaceId !== workspaceValue.id)
|
|
10871
|
-
return failure2("recovery_required", "task recovery binding is invalid");
|
|
10872
|
-
}
|
|
10873
|
-
const lock = this.readLockSnapshot();
|
|
10874
|
-
if (!lock.ok)
|
|
10875
|
-
return lock;
|
|
10876
|
-
const writer = target === "workspace" ? selectedRecord.data.writer : workspaceValue?.writer ?? null;
|
|
10877
|
-
const evidence = this.processEvidence(input, lock.data?.data ?? null, writer);
|
|
10878
|
-
if (!evidence.ok)
|
|
10879
|
-
return evidence;
|
|
10880
|
-
if (lock.data && (evidence.data.pid !== lock.data.data.pid || evidence.data.processStart !== lock.data.data.processStart))
|
|
10881
|
-
return failure2("recovery_required", "process evidence does not match metadata lock");
|
|
10882
|
-
if (writer && (evidence.data.state !== "accounted_for" || evidence.data.ownerDigest !== sha256(canonicalJson(writer))))
|
|
10883
|
-
return failure2("recovery_required", "workspace writer is not accounted for");
|
|
10884
|
-
const recoveryGate = { reclaimed: false };
|
|
10885
|
-
const result = this.withLock((handle) => {
|
|
10886
|
-
if (lock.data && !recoveryGate.reclaimed)
|
|
10887
|
-
return failure2("recovery_required", "metadata lock changed during recovery", {
|
|
10888
|
-
path: this.lockPath
|
|
10889
|
-
});
|
|
10890
|
-
if (!handle.verifyStillHeld())
|
|
10891
|
-
return failure2("recovery_required", "metadata lock was compromised", {
|
|
10892
|
-
path: this.lockPath
|
|
10893
|
-
});
|
|
10894
|
-
const reacquiredBytes = this.snapshotBytes(file);
|
|
10895
|
-
if (!reacquiredBytes)
|
|
10896
|
-
return failure2("recovery_required", "snapshot disappeared during recovery");
|
|
10897
|
-
if (digestBytes(reacquiredBytes) !== input.expectedBytes)
|
|
10898
|
-
return failure2("revision_conflict", "snapshot changed during recovery");
|
|
10899
|
-
if (target === "task") {
|
|
10900
|
-
const currentWorkspace = this.readWorkspace();
|
|
10901
|
-
if (!currentWorkspace.ok || !currentWorkspace.data)
|
|
10902
|
-
return failure2("recovery_required", "workspace changed during recovery");
|
|
10903
|
-
if (currentWorkspace.data.revision !== input.expectedWorkspaceRevision)
|
|
10904
|
-
return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
|
|
10905
|
-
}
|
|
10906
|
-
if (target === "workspace") {
|
|
10907
|
-
const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
|
|
10908
|
-
if (!parsed.ok)
|
|
10909
|
-
return parsed;
|
|
10910
|
-
const value = { ...parsed.data, revision: newRevision(), writer: null };
|
|
10911
|
-
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
10912
|
-
return replaced.ok ? success(value.revision, value.revision, value) : replaced;
|
|
10913
|
-
}
|
|
10914
|
-
const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
10915
|
-
if (!parsed.ok)
|
|
10916
|
-
return parsed;
|
|
10917
|
-
const value = {
|
|
10918
|
-
...parsed.data,
|
|
10919
|
-
revision: newRevision(),
|
|
10920
|
-
updatedAt: now(),
|
|
10921
|
-
status: parsed.data.status === "active" ? "paused" : parsed.data.status
|
|
10922
|
-
};
|
|
10923
|
-
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
10924
|
-
return replaced.ok ? success(value.revision, null, value) : replaced;
|
|
10925
|
-
}, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
|
|
10926
|
-
return result;
|
|
10927
|
-
} catch (error) {
|
|
10928
|
-
return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
|
|
10929
|
-
}
|
|
10930
|
-
}
|
|
10931
|
-
processEvidence(input, lock, writer) {
|
|
10932
|
-
const lockCopy = lock ? this.deepFreeze(structuredClone(lock)) : null;
|
|
10933
|
-
const writerCopy = writer ? this.deepFreeze(structuredClone(writer)) : null;
|
|
10934
|
-
const lockDigest = lockCopy ? sha256(canonicalJson(lockCopy)) : null;
|
|
10935
|
-
const writerDigest = writerCopy ? sha256(canonicalJson(writerCopy)) : null;
|
|
10936
|
-
try {
|
|
10937
|
-
const result = input.processEvidence(lockCopy, writerCopy);
|
|
10938
|
-
if (lockCopy && sha256(canonicalJson(lockCopy)) !== lockDigest)
|
|
10939
|
-
return failure2("recovery_required", "process evidence mutated lock identity");
|
|
10940
|
-
if (writerCopy && sha256(canonicalJson(writerCopy)) !== writerDigest)
|
|
10941
|
-
return failure2("recovery_required", "process evidence mutated writer identity");
|
|
10942
|
-
if (!result.ok)
|
|
10943
|
-
return result;
|
|
10944
|
-
const parsed = processEvidenceSchema.safeParse(result.data);
|
|
10945
|
-
return parsed.success ? success(null, null, parsed.data) : failure2("recovery_required", "process evidence is invalid");
|
|
10946
|
-
} catch (error) {
|
|
10947
|
-
return failure2("recovery_required", `process evidence failed: ${String(error)}`);
|
|
10948
|
-
}
|
|
10949
|
-
}
|
|
10950
|
-
deepFreeze(value) {
|
|
10951
|
-
if (value && typeof value === "object") {
|
|
10952
|
-
for (const child of Object.values(value))
|
|
10953
|
-
this.deepFreeze(child);
|
|
10954
|
-
Object.freeze(value);
|
|
10955
|
-
}
|
|
10956
|
-
return value;
|
|
10957
|
-
}
|
|
10958
|
-
readLockSnapshot() {
|
|
10959
|
-
try {
|
|
10960
|
-
const stat = fs6.lstatSync(this.lockPath);
|
|
10961
|
-
if (!stat.isFile() || stat.isSymbolicLink())
|
|
10962
|
-
return failure2("recovery_required", "metadata lock is not a regular file", {
|
|
10963
|
-
path: this.lockPath
|
|
10964
|
-
});
|
|
10965
|
-
const raw = fs6.readFileSync(this.lockPath, "utf8");
|
|
10966
|
-
return success(null, null, { raw, data: parseMetadataLock(raw) });
|
|
10967
|
-
} catch (error) {
|
|
10968
|
-
if (error?.code === "ENOENT")
|
|
10969
|
-
return success(null, null, null);
|
|
10970
|
-
return failure2("recovery_required", `metadata lock is invalid: ${String(error)}`, {
|
|
10971
|
-
path: this.lockPath
|
|
10972
|
-
});
|
|
10973
|
-
}
|
|
10974
|
-
}
|
|
10975
|
-
metadataLockOptions() {
|
|
10976
|
-
const inProcess = heldInProcess.has(this.root);
|
|
10977
|
-
return {
|
|
10978
|
-
lockPath: this.lockPath,
|
|
10979
|
-
staleMs: Number.MAX_SAFE_INTEGER,
|
|
10980
|
-
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
10981
|
-
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
10982
|
-
staleRecovery: "remove-if-unchanged",
|
|
10983
|
-
shouldReclaim: ({ payload, nowMs }) => {
|
|
10984
|
-
let ageMs = null;
|
|
10985
|
-
try {
|
|
10986
|
-
ageMs = nowMs - fs6.lstatSync(this.lockPath).mtimeMs;
|
|
10987
|
-
} catch {}
|
|
10988
|
-
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
10989
|
-
},
|
|
10990
|
-
shouldRemoveStaleLock: () => true,
|
|
10991
|
-
parsePayload: parseMetadataLockOrNull,
|
|
10992
|
-
payload: () => ({
|
|
10993
|
-
pid: process.pid,
|
|
10994
|
-
processStart: processStartOf(process.pid),
|
|
10995
|
-
host: localLockHost(),
|
|
10996
|
-
nonce: randomUUID2()
|
|
10997
|
-
})
|
|
10998
|
-
};
|
|
10999
|
-
}
|
|
11000
|
-
acquireMetadataLock(options) {
|
|
11001
|
-
clearAbandonedReclaimGuard(this.lockPath);
|
|
11002
|
-
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
11003
|
-
while (true) {
|
|
11004
|
-
try {
|
|
11005
|
-
return acquireFileLockSync(this.workspacePath, {
|
|
11006
|
-
...options,
|
|
11007
|
-
timeoutMs: Math.max(0, deadline - Date.now())
|
|
11008
|
-
});
|
|
11009
|
-
} catch (error) {
|
|
11010
|
-
const code = error?.code;
|
|
11011
|
-
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
11012
|
-
throw error;
|
|
11013
|
-
if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
|
|
11014
|
-
throw error;
|
|
11015
|
-
if (code !== "file_lock_stale")
|
|
11016
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
11017
|
-
}
|
|
11018
|
-
}
|
|
11019
|
-
}
|
|
11020
|
-
externalActionLockOptions() {
|
|
11021
|
-
const options = this.metadataLockOptions();
|
|
11022
|
-
return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
|
|
11436
|
+
}));
|
|
11437
|
+
this.compactPending();
|
|
11438
|
+
return result;
|
|
11023
11439
|
}
|
|
11024
11440
|
async withExternalActionLock(operation, reentrant = false) {
|
|
11025
11441
|
const activeRoots = externalActionLockRoots.getStore();
|
|
11026
11442
|
if (activeRoots && [...activeRoots].some((root) => root === this.root || sameDirectoryIdentity(root, this.root)))
|
|
11027
11443
|
return operation();
|
|
11444
|
+
const ready = this.ready(true);
|
|
11445
|
+
if (!ready.ok)
|
|
11446
|
+
return ready;
|
|
11447
|
+
const lockPath = this.paths().checkoutLock;
|
|
11028
11448
|
try {
|
|
11029
|
-
this.
|
|
11449
|
+
this.initializeStorage();
|
|
11030
11450
|
} catch (error) {
|
|
11031
11451
|
return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
|
|
11032
|
-
path: this.
|
|
11452
|
+
path: this.paths().dir
|
|
11033
11453
|
});
|
|
11034
11454
|
}
|
|
11035
11455
|
let handle;
|
|
11036
11456
|
try {
|
|
11037
|
-
|
|
11457
|
+
const options = this.lockOptions(lockPath);
|
|
11458
|
+
handle = this.acquire(lockPath, {
|
|
11459
|
+
...options,
|
|
11460
|
+
payload: () => ({ ...options.payload(), externalAction: true })
|
|
11461
|
+
});
|
|
11038
11462
|
} catch (error) {
|
|
11039
|
-
return this.lockFailure(error);
|
|
11463
|
+
return this.lockFailure(lockPath, error);
|
|
11040
11464
|
}
|
|
11041
|
-
|
|
11465
|
+
hold(lockPath);
|
|
11042
11466
|
let result;
|
|
11043
11467
|
try {
|
|
11044
11468
|
if (!handle.verifyStillHeld())
|
|
11045
|
-
result = failure2("recovery_required", "metadata lock was compromised", {
|
|
11046
|
-
path: this.lockPath
|
|
11047
|
-
});
|
|
11469
|
+
result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
|
|
11048
11470
|
else {
|
|
11049
11471
|
try {
|
|
11050
11472
|
const run = () => operation();
|
|
@@ -11058,526 +11480,1144 @@ class TaskStore {
|
|
|
11058
11480
|
if (!handle.verifyStillHeld())
|
|
11059
11481
|
result = failure2("external_outcome_unknown", "metadata lock was compromised", {
|
|
11060
11482
|
outcome: "unknown",
|
|
11061
|
-
path:
|
|
11483
|
+
path: lockPath
|
|
11062
11484
|
});
|
|
11063
11485
|
} catch {
|
|
11064
11486
|
result = failure2("external_outcome_unknown", "metadata lock could not be verified", {
|
|
11065
11487
|
outcome: "unknown",
|
|
11066
|
-
path:
|
|
11488
|
+
path: lockPath
|
|
11067
11489
|
});
|
|
11068
11490
|
}
|
|
11069
11491
|
}
|
|
11070
11492
|
} catch {
|
|
11071
11493
|
result = failure2("external_outcome_unknown", "external action lock operation failed", {
|
|
11072
11494
|
outcome: "unknown",
|
|
11073
|
-
path:
|
|
11495
|
+
path: lockPath
|
|
11074
11496
|
});
|
|
11075
11497
|
}
|
|
11076
|
-
|
|
11498
|
+
drop(lockPath);
|
|
11077
11499
|
try {
|
|
11078
11500
|
retryTransient(() => handle.release());
|
|
11079
11501
|
} catch (error) {
|
|
11080
|
-
const released = this.lockFailure(error);
|
|
11502
|
+
const released = this.lockFailure(lockPath, error);
|
|
11081
11503
|
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
11082
11504
|
return result.ok ? failure2("external_outcome_unknown", "external action metadata lock release is uncertain", {
|
|
11083
11505
|
outcome: "unknown",
|
|
11084
|
-
path:
|
|
11506
|
+
path: lockPath
|
|
11085
11507
|
}) : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
11086
11508
|
}
|
|
11087
11509
|
return result;
|
|
11088
11510
|
}
|
|
11089
|
-
|
|
11090
|
-
const
|
|
11091
|
-
options.
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11511
|
+
collectGarbage(options = {}) {
|
|
11512
|
+
const dryRun = options.dryRun === true;
|
|
11513
|
+
const compactAbove = options.compactAbove ?? COMPACT_ABOVE;
|
|
11514
|
+
const report = {
|
|
11515
|
+
dryRun,
|
|
11516
|
+
compacted: { tasks: [], eventsFolded: 0, bytesBefore: 0, bytesAfter: 0 },
|
|
11517
|
+
blobs: { removed: 0, removedBytes: 0, kept: 0 },
|
|
11518
|
+
temporary: { removed: 0 },
|
|
11519
|
+
legacyRecovery: null,
|
|
11520
|
+
retried: [],
|
|
11521
|
+
failed: []
|
|
11522
|
+
};
|
|
11523
|
+
const ready = this.ready(!dryRun);
|
|
11524
|
+
if (!ready.ok)
|
|
11525
|
+
return ready;
|
|
11526
|
+
const dirs = this.paths();
|
|
11527
|
+
const recovery = path9.join(this.root, ".workit", "recovery");
|
|
11528
|
+
if (fs8.existsSync(recovery)) {
|
|
11529
|
+
let files = 0;
|
|
11530
|
+
let bytes = 0;
|
|
11531
|
+
try {
|
|
11532
|
+
for (const name of fs8.readdirSync(recovery)) {
|
|
11533
|
+
const stat = fs8.lstatSync(path9.join(recovery, name));
|
|
11534
|
+
if (!stat.isFile())
|
|
11535
|
+
continue;
|
|
11536
|
+
files += 1;
|
|
11537
|
+
bytes += stat.size;
|
|
11538
|
+
}
|
|
11539
|
+
} catch {}
|
|
11540
|
+
const remove = options.pruneRecovery === true && !dryRun;
|
|
11541
|
+
if (remove)
|
|
11542
|
+
fs8.rmSync(recovery, { recursive: true, force: true });
|
|
11543
|
+
report.legacyRecovery = { path: recovery, files, bytes, removed: remove };
|
|
11544
|
+
}
|
|
11545
|
+
if (!fs8.existsSync(dirs.tasks))
|
|
11546
|
+
return success(null, null, report);
|
|
11547
|
+
const names = fs8.readdirSync(dirs.tasks).filter((name) => validId(name));
|
|
11548
|
+
for (const name of names) {
|
|
11549
|
+
const file = this.eventsPath(name);
|
|
11550
|
+
let size = 0;
|
|
11551
|
+
try {
|
|
11552
|
+
size = fs8.statSync(file).size;
|
|
11553
|
+
} catch {
|
|
11554
|
+
continue;
|
|
11555
|
+
}
|
|
11556
|
+
const lines = this.countEvents(file);
|
|
11557
|
+
if (lines > compactAbove) {
|
|
11558
|
+
if (dryRun) {
|
|
11559
|
+
report.compacted.tasks.push(name);
|
|
11560
|
+
report.compacted.eventsFolded += lines - COMPACT_KEEP;
|
|
11561
|
+
report.compacted.bytesBefore += size;
|
|
11562
|
+
} else {
|
|
11563
|
+
const compacted = this.compact(name);
|
|
11564
|
+
if (!compacted.ok)
|
|
11565
|
+
(compacted.code === "busy" ? report.retried : report.failed).push(name);
|
|
11566
|
+
else {
|
|
11567
|
+
report.compacted.tasks.push(name);
|
|
11568
|
+
report.compacted.eventsFolded += compacted.data.folded;
|
|
11569
|
+
report.compacted.bytesBefore += size;
|
|
11570
|
+
report.compacted.bytesAfter += compacted.data.bytes;
|
|
11571
|
+
}
|
|
11572
|
+
}
|
|
11573
|
+
}
|
|
11574
|
+
}
|
|
11575
|
+
const nowMs = Date.now();
|
|
11576
|
+
const sweep = () => {
|
|
11577
|
+
const references = new Set;
|
|
11578
|
+
for (const name of names)
|
|
11579
|
+
try {
|
|
11580
|
+
for (const match of fs8.readFileSync(this.eventsPath(name), "utf8").matchAll(/"\$blob":"([0-9a-f]{64})"/g))
|
|
11581
|
+
references.add(match[1]);
|
|
11582
|
+
} catch {}
|
|
11583
|
+
if (fs8.existsSync(dirs.blobs))
|
|
11584
|
+
for (const name of fs8.readdirSync(dirs.blobs)) {
|
|
11585
|
+
const digest = name.replace(/\.json$/, "");
|
|
11586
|
+
const file = path9.join(dirs.blobs, name);
|
|
11587
|
+
let stat;
|
|
11588
|
+
try {
|
|
11589
|
+
stat = fs8.lstatSync(file);
|
|
11590
|
+
} catch {
|
|
11591
|
+
continue;
|
|
11592
|
+
}
|
|
11593
|
+
if (references.has(digest) || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS) {
|
|
11594
|
+
report.blobs.kept += 1;
|
|
11595
|
+
continue;
|
|
11596
|
+
}
|
|
11597
|
+
report.blobs.removed += 1;
|
|
11598
|
+
report.blobs.removedBytes += stat.size;
|
|
11599
|
+
if (!dryRun)
|
|
11600
|
+
fs8.rmSync(file, { force: true });
|
|
11601
|
+
}
|
|
11602
|
+
return success(null, null, null);
|
|
11101
11603
|
};
|
|
11102
|
-
|
|
11604
|
+
const swept = dryRun ? sweep() : this.withLock(path9.join(dirs.dir, "gc.lock"), sweep);
|
|
11605
|
+
if (!swept.ok)
|
|
11606
|
+
return swept;
|
|
11607
|
+
const temporaryDirs = [
|
|
11608
|
+
dirs.dir,
|
|
11609
|
+
dirs.tasks,
|
|
11610
|
+
dirs.blobs,
|
|
11611
|
+
dirs.checkout,
|
|
11612
|
+
...names.map((name) => path9.join(dirs.tasks, name))
|
|
11613
|
+
];
|
|
11614
|
+
for (const directory of temporaryDirs) {
|
|
11615
|
+
if (!fs8.existsSync(directory))
|
|
11616
|
+
continue;
|
|
11617
|
+
for (const name of fs8.readdirSync(directory)) {
|
|
11618
|
+
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
11619
|
+
continue;
|
|
11620
|
+
const file = path9.join(directory, name);
|
|
11621
|
+
try {
|
|
11622
|
+
const stat = fs8.lstatSync(file);
|
|
11623
|
+
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
11624
|
+
continue;
|
|
11625
|
+
} catch {
|
|
11626
|
+
continue;
|
|
11627
|
+
}
|
|
11628
|
+
report.temporary.removed += 1;
|
|
11629
|
+
if (!dryRun)
|
|
11630
|
+
fs8.rmSync(file, { force: true });
|
|
11631
|
+
}
|
|
11632
|
+
}
|
|
11633
|
+
return success(null, null, report);
|
|
11103
11634
|
}
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
const
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11635
|
+
paths() {
|
|
11636
|
+
const location = this.location();
|
|
11637
|
+
const dir = location.ok ? location.data.dir : path9.join(this.root, ".workit");
|
|
11638
|
+
const checkout = path9.join(dir, "checkouts", checkoutSlug(this.root));
|
|
11639
|
+
return {
|
|
11640
|
+
dir,
|
|
11641
|
+
shared: location.ok ? location.data.shared : false,
|
|
11642
|
+
marker: path9.join(dir, "store.json"),
|
|
11643
|
+
checkout,
|
|
11644
|
+
workspace: path9.join(checkout, "workspace.json"),
|
|
11645
|
+
checkoutLock: path9.join(checkout, "metadata.lock"),
|
|
11646
|
+
tasks: path9.join(dir, "tasks"),
|
|
11647
|
+
index: path9.join(dir, "task-index.json"),
|
|
11648
|
+
blobs: path9.join(dir, "blobs", "candidates"),
|
|
11649
|
+
legacy: path9.join(dir, "legacy", checkoutSlug(this.root), "v2"),
|
|
11650
|
+
v2: path9.join(this.root, ".workit")
|
|
11112
11651
|
};
|
|
11113
|
-
this.replaceSnapshot(this.workspacePath, value, workspace);
|
|
11114
11652
|
}
|
|
11115
|
-
|
|
11653
|
+
eventsPath(taskId) {
|
|
11654
|
+
return path9.join(this.paths().tasks, taskId, "events.jsonl");
|
|
11655
|
+
}
|
|
11656
|
+
snapshotPath(taskId) {
|
|
11657
|
+
return path9.join(this.paths().tasks, taskId, "snapshot.json");
|
|
11658
|
+
}
|
|
11659
|
+
taskLockPath(taskId) {
|
|
11660
|
+
return path9.join(this.paths().tasks, taskId, "lock");
|
|
11661
|
+
}
|
|
11662
|
+
ready(write = false) {
|
|
11663
|
+
const location = this.location();
|
|
11664
|
+
if (!location.ok)
|
|
11665
|
+
return location;
|
|
11666
|
+
if (this.migrationChecked)
|
|
11667
|
+
return success(null, null, null);
|
|
11668
|
+
const dirs = this.paths();
|
|
11116
11669
|
try {
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11670
|
+
const marker = JSON.parse(fs8.readFileSync(dirs.marker, "utf8"));
|
|
11671
|
+
if (typeof marker.version === "number" && marker.version > STORE_FORMAT)
|
|
11672
|
+
return failure2("recovery_required", `the workit store at ${dirs.dir} has format ${marker.version}; upgrade Workit to use it`, { path: dirs.marker });
|
|
11673
|
+
} catch {}
|
|
11674
|
+
this.migrationChecked = true;
|
|
11675
|
+
const settled = this.settleMigrations(write);
|
|
11676
|
+
if (!settled.ok)
|
|
11677
|
+
this.migrationChecked = false;
|
|
11678
|
+
return settled;
|
|
11679
|
+
}
|
|
11680
|
+
settleMigrations(write) {
|
|
11681
|
+
const moved = this.adoptLocalStore();
|
|
11682
|
+
if (!moved.ok)
|
|
11683
|
+
return moved;
|
|
11684
|
+
const pending = this.pendingMigrations();
|
|
11685
|
+
if (pending.length > 0 && !write && !(this.migrateOnRead ?? migrationReporterInstalled()))
|
|
11686
|
+
return failure2("needs_input", `${MIGRATION_PENDING}: the task store at ${pending[0]} has not been migrated yet; run \`workit task status\` to migrate it`, { path: pending[0], guidance: "workit task status" });
|
|
11687
|
+
for (const v2 of this.v2Dirs()) {
|
|
11688
|
+
const migrated = this.migrateV2(v2);
|
|
11689
|
+
if (!migrated.ok)
|
|
11690
|
+
return migrated;
|
|
11122
11691
|
}
|
|
11123
|
-
|
|
11124
|
-
|
|
11692
|
+
return success(null, null, null);
|
|
11693
|
+
}
|
|
11694
|
+
blobStore() {
|
|
11695
|
+
const dir = this.paths().blobs;
|
|
11696
|
+
return {
|
|
11697
|
+
read: (digest) => {
|
|
11698
|
+
try {
|
|
11699
|
+
return JSON.parse(fs8.readFileSync(path9.join(dir, `${digest}.json`), "utf8"));
|
|
11700
|
+
} catch (error) {
|
|
11701
|
+
throw new LogDamage(`stored candidate ${digest} is missing or unreadable: ${String(error)}`);
|
|
11702
|
+
}
|
|
11703
|
+
},
|
|
11704
|
+
write: (digest, value) => {
|
|
11705
|
+
const file = path9.join(dir, `${digest}.json`);
|
|
11706
|
+
if (fs8.existsSync(file)) {
|
|
11707
|
+
const stamp = new Date;
|
|
11708
|
+
try {
|
|
11709
|
+
fs8.utimesSync(file, stamp, stamp);
|
|
11710
|
+
return;
|
|
11711
|
+
} catch {}
|
|
11712
|
+
}
|
|
11713
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
11714
|
+
replaceFile(file, JSON.stringify(value), true);
|
|
11715
|
+
}
|
|
11716
|
+
};
|
|
11717
|
+
}
|
|
11718
|
+
load(taskId) {
|
|
11125
11719
|
try {
|
|
11126
|
-
|
|
11720
|
+
const loaded = this.loadOrThrow(taskId);
|
|
11721
|
+
if (!loaded)
|
|
11722
|
+
return success(null, null, null);
|
|
11723
|
+
const parsed = this.parseTask(loaded.state.record, taskId);
|
|
11724
|
+
if (!parsed.ok)
|
|
11725
|
+
return parsed;
|
|
11726
|
+
return success(null, null, {
|
|
11727
|
+
record: parsed.data,
|
|
11728
|
+
key: loaded.state.key,
|
|
11729
|
+
legacy: loaded.state.legacy,
|
|
11730
|
+
seq: loaded.seq,
|
|
11731
|
+
lastId: loaded.lastId,
|
|
11732
|
+
lastStart: loaded.lastStart,
|
|
11733
|
+
end: loaded.end,
|
|
11734
|
+
size: loaded.size,
|
|
11735
|
+
snapshotSeq: loaded.snapshotSeq
|
|
11736
|
+
});
|
|
11127
11737
|
} catch (error) {
|
|
11128
|
-
|
|
11738
|
+
if (error instanceof LogDamage)
|
|
11739
|
+
return failure2("recovery_required", error.upgrade ? error.message : `task ${taskId} log is damaged: ${error.message}`, { taskId, path: this.eventsPath(taskId) });
|
|
11740
|
+
return failure2("recovery_required", `task ${taskId} cannot be read: ${String(error)}`, {
|
|
11741
|
+
taskId
|
|
11742
|
+
});
|
|
11129
11743
|
}
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11744
|
+
}
|
|
11745
|
+
loadOrThrow(taskId) {
|
|
11746
|
+
const blobs = this.blobStore();
|
|
11747
|
+
const file = this.eventsPath(taskId);
|
|
11748
|
+
const snapshot = this.readSnapshot(taskId);
|
|
11749
|
+
if (snapshot) {
|
|
11133
11750
|
try {
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
}
|
|
11142
|
-
|
|
11143
|
-
|
|
11751
|
+
const tail = readLog(file, snapshot.lastStart, snapshot.seq);
|
|
11752
|
+
const first = tail?.events[0];
|
|
11753
|
+
if (tail && first && first.id === snapshot.lastId) {
|
|
11754
|
+
let state = {
|
|
11755
|
+
record: snapshot.record,
|
|
11756
|
+
key: snapshot.key,
|
|
11757
|
+
legacy: snapshot.legacy
|
|
11758
|
+
};
|
|
11759
|
+
for (const event of tail.events.slice(1))
|
|
11760
|
+
state = reduce(state, event, blobs);
|
|
11761
|
+
return this.loadedFrom(state, tail, snapshot.seq);
|
|
11144
11762
|
}
|
|
11145
11763
|
} catch (error) {
|
|
11146
|
-
|
|
11764
|
+
if (error instanceof LogDamage && error.upgrade)
|
|
11765
|
+
throw error;
|
|
11147
11766
|
}
|
|
11148
11767
|
}
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
const releaseFailure = this.lockFailure(error);
|
|
11155
|
-
const releaseError = releaseFailure.ok ? "metadata lock release failed" : releaseFailure.error;
|
|
11156
|
-
result = result.ok ? releaseFailure : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
11157
|
-
}
|
|
11768
|
+
const log = readLog(file, 0, null);
|
|
11769
|
+
if (!log || log.events.length === 0) {
|
|
11770
|
+
if (log && log.size > 0)
|
|
11771
|
+
throw new LogDamage("the log holds no complete event");
|
|
11772
|
+
return null;
|
|
11158
11773
|
}
|
|
11159
|
-
|
|
11774
|
+
if (!OPENING_TYPES.has(log.events[0].type))
|
|
11775
|
+
throw new LogDamage(`the log starts with ${log.events[0].type}, not an opening event`);
|
|
11776
|
+
let state = null;
|
|
11777
|
+
for (const event of log.events)
|
|
11778
|
+
state = reduce(state, event, blobs);
|
|
11779
|
+
return this.loadedFrom(state, log, 0);
|
|
11780
|
+
}
|
|
11781
|
+
loadedFrom(state, log, snapshotSeq) {
|
|
11782
|
+
const last = log.events.at(-1);
|
|
11783
|
+
return {
|
|
11784
|
+
state,
|
|
11785
|
+
seq: last.seq,
|
|
11786
|
+
lastId: last.id,
|
|
11787
|
+
lastStart: log.lastStart,
|
|
11788
|
+
end: log.end,
|
|
11789
|
+
size: log.size,
|
|
11790
|
+
snapshotSeq
|
|
11791
|
+
};
|
|
11160
11792
|
}
|
|
11161
|
-
|
|
11162
|
-
const probe = `${this.lockPath}.${process.pid}.${randomUUID2()}.probe`;
|
|
11793
|
+
readSnapshot(taskId) {
|
|
11163
11794
|
try {
|
|
11164
|
-
|
|
11795
|
+
const value = JSON.parse(fs8.readFileSync(this.snapshotPath(taskId), "utf8"));
|
|
11796
|
+
if (!isObject4(value) || value.v !== 1 || value.task !== taskId || typeof value.seq !== "number" || typeof value.lastId !== "string" || typeof value.lastStart !== "number" || typeof value.end !== "number" || !isObject4(value.record))
|
|
11797
|
+
return null;
|
|
11798
|
+
return value;
|
|
11165
11799
|
} catch {
|
|
11166
|
-
return
|
|
11800
|
+
return null;
|
|
11167
11801
|
}
|
|
11802
|
+
}
|
|
11803
|
+
writeSnapshot(taskId, loaded) {
|
|
11804
|
+
const snapshot = {
|
|
11805
|
+
v: 1,
|
|
11806
|
+
task: taskId,
|
|
11807
|
+
seq: loaded.seq,
|
|
11808
|
+
lastId: loaded.lastId,
|
|
11809
|
+
lastStart: loaded.lastStart,
|
|
11810
|
+
end: loaded.end,
|
|
11811
|
+
key: loaded.key,
|
|
11812
|
+
legacy: loaded.legacy,
|
|
11813
|
+
record: loaded.record
|
|
11814
|
+
};
|
|
11168
11815
|
try {
|
|
11169
|
-
|
|
11816
|
+
replaceFile(this.snapshotPath(taskId), JSON.stringify(snapshot), false);
|
|
11170
11817
|
} catch {}
|
|
11171
|
-
return true;
|
|
11172
11818
|
}
|
|
11173
|
-
|
|
11819
|
+
loadBound(taskId) {
|
|
11820
|
+
const loaded = this.load(taskId);
|
|
11821
|
+
if (!loaded.ok)
|
|
11822
|
+
return loaded;
|
|
11823
|
+
if (!loaded.data)
|
|
11824
|
+
return failure2("not_found", "task not found", { taskId });
|
|
11825
|
+
const workspace = this.readWorkspace();
|
|
11826
|
+
if (!workspace.ok)
|
|
11827
|
+
return workspace;
|
|
11828
|
+
const bound = this.bindingError(loaded.data.record, workspace.data);
|
|
11829
|
+
return bound ?? success(null, null, loaded.data);
|
|
11830
|
+
}
|
|
11831
|
+
bindingError(task, workspace) {
|
|
11832
|
+
if (workspace && task.workspaceId === workspace.id)
|
|
11833
|
+
return null;
|
|
11834
|
+
return failure2("not_found", `task ${task.id} belongs to another checkout of this repository; run there, or move it here with \`workit task adopt ${task.id}\``, { taskId: task.id });
|
|
11835
|
+
}
|
|
11836
|
+
nextTask(current, changed, context) {
|
|
11837
|
+
const valid = taskRecordSchema.safeParse({
|
|
11838
|
+
...changed,
|
|
11839
|
+
id: current.id,
|
|
11840
|
+
workspaceId: current.workspaceId,
|
|
11841
|
+
createdAt: current.createdAt,
|
|
11842
|
+
revision: context.revision,
|
|
11843
|
+
updatedAt: context.now,
|
|
11844
|
+
runtime: stampNew(current)
|
|
11845
|
+
});
|
|
11846
|
+
return valid.success ? valid.data : null;
|
|
11847
|
+
}
|
|
11848
|
+
event(taskId, seq, type, data) {
|
|
11849
|
+
return {
|
|
11850
|
+
v: EVENT_VERSION,
|
|
11851
|
+
seq,
|
|
11852
|
+
at: new Date().toISOString(),
|
|
11853
|
+
id: newId(),
|
|
11854
|
+
task: taskId,
|
|
11855
|
+
actor: this.actor,
|
|
11856
|
+
type,
|
|
11857
|
+
data
|
|
11858
|
+
};
|
|
11859
|
+
}
|
|
11860
|
+
appendPatch(loaded, next, extra = {}) {
|
|
11861
|
+
const taskId = next.id;
|
|
11174
11862
|
try {
|
|
11175
|
-
|
|
11176
|
-
|
|
11863
|
+
const blobs = this.blobStore();
|
|
11864
|
+
const before = loaded.record;
|
|
11865
|
+
const after = next;
|
|
11866
|
+
const ops = encodeOps(diff(before, after), blobs);
|
|
11867
|
+
const { type, ...data } = extra;
|
|
11868
|
+
if (data.key)
|
|
11869
|
+
data.key = { ...data.key, boundAt: data.key.boundAt ?? new Date().toISOString() };
|
|
11870
|
+
const event = this.event(taskId, loaded.seq + 1, type ?? ("key" in extra ? "task.bound" : eventType(before, after)), { ops, ...data });
|
|
11871
|
+
const start = appendEvent(this.eventsPath(taskId), event, loaded.end);
|
|
11872
|
+
const end = start + Buffer.byteLength(`${JSON.stringify(event)}
|
|
11873
|
+
`);
|
|
11874
|
+
const key = "key" in data ? data.key ?? null : loaded.key;
|
|
11875
|
+
const state = {
|
|
11876
|
+
record: next,
|
|
11877
|
+
key,
|
|
11878
|
+
legacy: extra.legacy ?? loaded.legacy,
|
|
11879
|
+
seq: event.seq,
|
|
11880
|
+
lastId: event.id,
|
|
11881
|
+
lastStart: start,
|
|
11882
|
+
end
|
|
11883
|
+
};
|
|
11884
|
+
if (event.seq - loaded.snapshotSeq >= SNAPSHOT_EVERY || next.status !== loaded.record.status || loaded.snapshotSeq === 0)
|
|
11885
|
+
this.writeSnapshot(taskId, state);
|
|
11886
|
+
if (end > AUTO_COMPACT_BYTES)
|
|
11887
|
+
this.compactDue.add(taskId);
|
|
11888
|
+
this.indexTask(next, key, (extra.legacy ?? loaded.legacy) !== null);
|
|
11889
|
+
return success(null, null, null);
|
|
11177
11890
|
} catch (error) {
|
|
11178
|
-
return error
|
|
11891
|
+
return failure2("storage_error", `task append failed: ${String(error)}`, {
|
|
11892
|
+
path: this.eventsPath(taskId)
|
|
11893
|
+
});
|
|
11179
11894
|
}
|
|
11180
11895
|
}
|
|
11181
|
-
|
|
11182
|
-
const
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11896
|
+
openTask(record, type, key, legacy = null) {
|
|
11897
|
+
const dir = path9.join(this.paths().tasks, record.id);
|
|
11898
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
11899
|
+
const blobs = this.blobStore();
|
|
11900
|
+
const bound = key ? { ...key, boundAt: key.boundAt ?? new Date().toISOString() } : null;
|
|
11901
|
+
const event = this.event(record.id, 1, type, {
|
|
11902
|
+
record: encodeRecord(record, blobs),
|
|
11903
|
+
key: bound,
|
|
11904
|
+
...legacy ? { legacy } : {}
|
|
11905
|
+
});
|
|
11906
|
+
const file = this.eventsPath(record.id);
|
|
11907
|
+
const existing = readLog(file, 0, null);
|
|
11908
|
+
if (existing && existing.events.length > 0)
|
|
11909
|
+
throw new Error(`task ${record.id} already has a log`);
|
|
11910
|
+
appendEvent(file, event, 0);
|
|
11911
|
+
this.indexTask(record, bound, legacy !== null);
|
|
11912
|
+
}
|
|
11913
|
+
compactPending() {
|
|
11914
|
+
for (const taskId of this.compactDue) {
|
|
11915
|
+
this.compactDue.delete(taskId);
|
|
11916
|
+
this.compact(taskId);
|
|
11917
|
+
}
|
|
11918
|
+
}
|
|
11919
|
+
compact(taskId) {
|
|
11920
|
+
try {
|
|
11921
|
+
const file = this.eventsPath(taskId);
|
|
11922
|
+
const log = readLog(file, 0, null);
|
|
11923
|
+
if (!log || log.events.length <= COMPACT_KEEP + 1)
|
|
11924
|
+
return success(null, null, { folded: 0, bytes: log?.size ?? 0 });
|
|
11925
|
+
const blobs = this.blobStore();
|
|
11926
|
+
const cut = log.events.length - COMPACT_KEEP;
|
|
11927
|
+
let state = null;
|
|
11928
|
+
for (const event of log.events.slice(0, cut))
|
|
11929
|
+
state = reduce(state, event, blobs);
|
|
11930
|
+
const folded = log.events[cut - 1];
|
|
11931
|
+
const checkpoint = {
|
|
11932
|
+
...this.event(taskId, folded.seq, "task.checkpoint", {
|
|
11933
|
+
record: encodeRecord(state.record, blobs),
|
|
11934
|
+
key: state.key,
|
|
11935
|
+
...state.legacy ? { legacy: state.legacy } : {},
|
|
11936
|
+
folded: { from: log.events[0].seq, to: folded.seq }
|
|
11937
|
+
})
|
|
11938
|
+
};
|
|
11939
|
+
const prefix = `${[checkpoint, ...log.events.slice(cut)].map((event) => JSON.stringify(event)).join(`
|
|
11940
|
+
`)}
|
|
11941
|
+
`;
|
|
11942
|
+
const last = log.events.at(-1);
|
|
11943
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
11944
|
+
let latest = null;
|
|
11945
|
+
try {
|
|
11946
|
+
latest = readLog(file, log.lastStart, last.seq);
|
|
11947
|
+
} catch {}
|
|
11948
|
+
if (!latest || latest.events[0]?.id !== last.id)
|
|
11949
|
+
return failure2("busy", "the task log changed during compaction; retry", { taskId });
|
|
11950
|
+
const appended = fs8.readFileSync(file).subarray(log.end, latest.end);
|
|
11951
|
+
const bytes = Buffer.concat([Buffer.from(prefix, "utf8"), appended]);
|
|
11952
|
+
replaceFile(file, bytes.toString("utf8"), true);
|
|
11953
|
+
try {
|
|
11954
|
+
fs8.rmSync(this.snapshotPath(taskId), { force: true });
|
|
11955
|
+
} catch {}
|
|
11956
|
+
const reloaded = this.load(taskId);
|
|
11957
|
+
if (reloaded.ok && reloaded.data)
|
|
11958
|
+
this.writeSnapshot(taskId, reloaded.data);
|
|
11959
|
+
return success(null, null, { folded: cut, bytes: bytes.length });
|
|
11188
11960
|
});
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
11202
|
-
|
|
11203
|
-
|
|
11961
|
+
} catch (error) {
|
|
11962
|
+
return failure2("storage_error", `compaction failed: ${String(error)}`, { taskId });
|
|
11963
|
+
}
|
|
11964
|
+
}
|
|
11965
|
+
countEvents(file) {
|
|
11966
|
+
try {
|
|
11967
|
+
const bytes = fs8.readFileSync(file);
|
|
11968
|
+
let count = 0;
|
|
11969
|
+
for (let index = bytes.indexOf(10);index >= 0; index = bytes.indexOf(10, index + 1))
|
|
11970
|
+
count += 1;
|
|
11971
|
+
return count;
|
|
11972
|
+
} catch {
|
|
11973
|
+
return 0;
|
|
11974
|
+
}
|
|
11975
|
+
}
|
|
11976
|
+
openTasksForKey(key) {
|
|
11977
|
+
const all = this.listStoreIndex();
|
|
11978
|
+
if (!all.ok)
|
|
11979
|
+
return all;
|
|
11980
|
+
const location = this.location();
|
|
11981
|
+
const renames = location.ok ? branchRenames(location.data) : [];
|
|
11982
|
+
return success(null, null, all.data.filter((entry) => entry.key !== null && entry.status !== "closed").filter((entry) => (renames.length ? followRenames(entry.key, entry.boundAt ?? entry.createdAt, renames) : entry.key) === key).toSorted(oldestFirst));
|
|
11983
|
+
}
|
|
11984
|
+
openTaskForKey(key) {
|
|
11985
|
+
const bound = this.openTasksForKey(key);
|
|
11986
|
+
return bound.ok ? success(null, null, bound.data[0] ?? null) : bound;
|
|
11987
|
+
}
|
|
11988
|
+
withKeyLock(key, operation) {
|
|
11989
|
+
const digest = createHash6("sha256").update(key).digest("hex").slice(0, 32);
|
|
11990
|
+
return this.withLock(path9.join(this.paths().dir, "keys", `${digest}.lock`), operation);
|
|
11991
|
+
}
|
|
11992
|
+
bindLocked(taskId, key, timestamp) {
|
|
11993
|
+
const current = this.readWorkspace();
|
|
11994
|
+
if (!current.ok)
|
|
11995
|
+
return current;
|
|
11996
|
+
let workspace = current.data;
|
|
11997
|
+
if (!workspace) {
|
|
11998
|
+
workspace = this.nextWorkspace(null);
|
|
11999
|
+
const written = this.writeWorkspace(workspace);
|
|
12000
|
+
if (!written.ok)
|
|
12001
|
+
return written;
|
|
12002
|
+
}
|
|
12003
|
+
const target = workspace;
|
|
12004
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
12005
|
+
const loaded = this.load(taskId);
|
|
12006
|
+
if (!loaded.ok)
|
|
12007
|
+
return loaded;
|
|
12008
|
+
if (!loaded.data)
|
|
12009
|
+
return failure2("not_found", "task not found", { taskId });
|
|
12010
|
+
if (loaded.data.record.status === "closed")
|
|
12011
|
+
return failure2("invalid_transition", "a closed task cannot be adopted", { taskId });
|
|
12012
|
+
if (loaded.data.record.workspaceId === target.id && loaded.data.key?.key === key.key)
|
|
12013
|
+
return success(loaded.data.record.revision, null, loaded.data.record);
|
|
12014
|
+
const next = taskRecordSchema.safeParse({
|
|
12015
|
+
...loaded.data.record,
|
|
12016
|
+
workspaceId: target.id,
|
|
12017
|
+
revision: newRevision(),
|
|
12018
|
+
updatedAt: timestamp ?? now(),
|
|
12019
|
+
runtime: stampNew(loaded.data.record)
|
|
12020
|
+
});
|
|
12021
|
+
if (!next.success)
|
|
12022
|
+
return failure2("invalid_input", "rebound task is invalid");
|
|
12023
|
+
const written = this.appendPatch(loaded.data, next.data, { key });
|
|
12024
|
+
return written.ok ? success(next.data.revision, null, next.data) : written;
|
|
12025
|
+
});
|
|
12026
|
+
}
|
|
12027
|
+
unbindLocked(taskId) {
|
|
12028
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
12029
|
+
const loaded = this.load(taskId);
|
|
12030
|
+
if (!loaded.ok)
|
|
12031
|
+
return loaded;
|
|
12032
|
+
if (!loaded.data || loaded.data.key === null)
|
|
12033
|
+
return success(null, null, null);
|
|
12034
|
+
return this.appendPatch(loaded.data, loaded.data.record, { key: null });
|
|
12035
|
+
});
|
|
12036
|
+
}
|
|
12037
|
+
createLocked(value, bindCurrent, key, checkRevision = true) {
|
|
12038
|
+
const current = this.readWorkspace();
|
|
12039
|
+
if (!current.ok)
|
|
12040
|
+
return current;
|
|
12041
|
+
if (checkRevision && (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null))
|
|
12042
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
12043
|
+
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
12044
|
+
actualWorkspaceRevision: current.data?.revision ?? null
|
|
12045
|
+
});
|
|
12046
|
+
if (!value.provenance)
|
|
12047
|
+
return failure2("invalid_input", "provenance is required");
|
|
12048
|
+
if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
|
|
12049
|
+
return failure2("invalid_input", "task intent or provenance is invalid");
|
|
12050
|
+
if (bindCurrent) {
|
|
12051
|
+
const currentKey = this.currentKey();
|
|
12052
|
+
if (currentKey.ok)
|
|
12053
|
+
return this.withKeyLock(currentKey.data.key, () => {
|
|
12054
|
+
const holder = this.openTaskForKey(currentKey.data.key);
|
|
12055
|
+
if (!holder.ok)
|
|
12056
|
+
return holder;
|
|
12057
|
+
if (holder.data) {
|
|
12058
|
+
const released = this.unbindLocked(holder.data.id);
|
|
12059
|
+
if (!released.ok)
|
|
12060
|
+
return released;
|
|
12061
|
+
}
|
|
12062
|
+
return this.newTask(value, current.data, currentKey.data);
|
|
11204
12063
|
});
|
|
11205
12064
|
}
|
|
11206
|
-
|
|
11207
|
-
|
|
12065
|
+
return this.newTask(value, current.data, key);
|
|
12066
|
+
}
|
|
12067
|
+
newTask(value, previous, binding) {
|
|
12068
|
+
const current = { data: previous };
|
|
12069
|
+
const workspace = this.nextWorkspace(current.data);
|
|
12070
|
+
const timestamp = value.now ?? now();
|
|
12071
|
+
const task = {
|
|
12072
|
+
schemaVersion: SCHEMA_VERSION,
|
|
12073
|
+
id: newId(),
|
|
12074
|
+
workspaceId: workspace.id,
|
|
12075
|
+
revision: newRevision(),
|
|
12076
|
+
createdAt: timestamp,
|
|
12077
|
+
updatedAt: timestamp,
|
|
12078
|
+
origin: null,
|
|
12079
|
+
intent: {
|
|
12080
|
+
id: newId(),
|
|
12081
|
+
recordedAt: timestamp,
|
|
12082
|
+
provenance: value.provenance,
|
|
12083
|
+
data: value.intent
|
|
12084
|
+
},
|
|
12085
|
+
constraints: [],
|
|
12086
|
+
status: "active",
|
|
12087
|
+
closure: null,
|
|
12088
|
+
progress: { summary: "", nextAction: null, blockers: [] },
|
|
12089
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
12090
|
+
assessments: [],
|
|
12091
|
+
policy: null,
|
|
12092
|
+
policyChanges: [],
|
|
12093
|
+
candidates: [],
|
|
12094
|
+
evidence: [],
|
|
12095
|
+
decisions: [],
|
|
12096
|
+
findings: [],
|
|
12097
|
+
workers: []
|
|
12098
|
+
};
|
|
12099
|
+
return this.writeNewTask(current.data, workspace, task, "task.opened", binding, {
|
|
12100
|
+
operation: "create"
|
|
12101
|
+
});
|
|
12102
|
+
}
|
|
12103
|
+
nextWorkspace(current, id) {
|
|
12104
|
+
return current ? { ...current, revision: newRevision(), root: this.root, runtime: stampNew(current) } : {
|
|
12105
|
+
schemaVersion: SCHEMA_VERSION,
|
|
12106
|
+
id: id ?? newId(),
|
|
12107
|
+
revision: newRevision(),
|
|
12108
|
+
root: this.root,
|
|
12109
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
12110
|
+
writer: null
|
|
12111
|
+
};
|
|
12112
|
+
}
|
|
12113
|
+
writeNewTask(previous, workspace, task, type, key, details) {
|
|
12114
|
+
const written = this.writeWorkspace(workspace);
|
|
12115
|
+
if (!written.ok)
|
|
12116
|
+
return written;
|
|
12117
|
+
try {
|
|
12118
|
+
this.openTask(task, type, key);
|
|
12119
|
+
} catch (error) {
|
|
12120
|
+
if (previous) {
|
|
12121
|
+
const restored = this.writeWorkspace(previous);
|
|
12122
|
+
if (!restored.ok)
|
|
12123
|
+
return failure2("external_outcome_unknown", `task ${details.operation} failed and workspace restoration is uncertain`, { ...details, outcome: "unknown" });
|
|
12124
|
+
}
|
|
12125
|
+
return failure2("external_outcome_unknown", `workspace written but the task log is uncertain: ${String(error)}`, { ...details, outcome: "unknown" });
|
|
12126
|
+
}
|
|
12127
|
+
return success(task.revision, workspace.revision, task);
|
|
12128
|
+
}
|
|
12129
|
+
writeWorkspace(record) {
|
|
12130
|
+
const file = this.paths().workspace;
|
|
12131
|
+
try {
|
|
12132
|
+
fs8.mkdirSync(path9.dirname(file), { recursive: true });
|
|
12133
|
+
replaceFile(file, `${canonicalJson(record)}
|
|
12134
|
+
`, true);
|
|
12135
|
+
return success(null, null, null);
|
|
12136
|
+
} catch (error) {
|
|
12137
|
+
return failure2("storage_error", `workspace write failed: ${String(error)}`, { path: file });
|
|
12138
|
+
}
|
|
12139
|
+
}
|
|
12140
|
+
markUncertain(workspace) {
|
|
12141
|
+
if (!workspace.writer || workspace.writer.state === "uncertain")
|
|
12142
|
+
return;
|
|
12143
|
+
this.writeWorkspace({
|
|
12144
|
+
...workspace,
|
|
12145
|
+
writer: { ...workspace.writer, state: "uncertain" },
|
|
12146
|
+
revision: newRevision(),
|
|
12147
|
+
runtime: stampNew(workspace)
|
|
12148
|
+
});
|
|
12149
|
+
}
|
|
12150
|
+
readIndex() {
|
|
12151
|
+
try {
|
|
12152
|
+
const value = JSON.parse(fs8.readFileSync(this.paths().index, "utf8"));
|
|
12153
|
+
if (!isObject4(value) || value.version !== INDEX_VERSION || !isObject4(value.tasks))
|
|
12154
|
+
return {};
|
|
12155
|
+
const tasks = {};
|
|
12156
|
+
for (const [id, entry] of Object.entries(value.tasks))
|
|
12157
|
+
if (isObject4(entry) && entry.id === id && typeof entry.file === "string" && typeof entry.status === "string" && typeof entry.updatedAt === "string" && typeof entry.workspaceId === "string" && Array.isArray(entry.sessions) && isObject4(entry.progress) && isObject4(entry.source))
|
|
12158
|
+
tasks[id] = entry;
|
|
12159
|
+
return tasks;
|
|
12160
|
+
} catch {
|
|
12161
|
+
return {};
|
|
12162
|
+
}
|
|
11208
12163
|
}
|
|
11209
|
-
|
|
11210
|
-
retryTransient(() => fs6.mkdirSync(this.tasksDir, { recursive: true }));
|
|
11211
|
-
retryTransient(() => fs6.mkdirSync(this.recoveryDir, { recursive: true }));
|
|
11212
|
-
let current = null;
|
|
12164
|
+
writeIndex(entries) {
|
|
11213
12165
|
try {
|
|
11214
|
-
|
|
12166
|
+
fs8.mkdirSync(this.paths().tasks, { recursive: true });
|
|
12167
|
+
replaceFile(this.paths().index, JSON.stringify({
|
|
12168
|
+
version: INDEX_VERSION,
|
|
12169
|
+
tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
|
|
12170
|
+
}), false);
|
|
12171
|
+
} catch {}
|
|
12172
|
+
}
|
|
12173
|
+
indexTask(task, key, legacy) {
|
|
12174
|
+
try {
|
|
12175
|
+
const signature = fileSignature(this.eventsPath(task.id));
|
|
12176
|
+
if (signature === null)
|
|
12177
|
+
return;
|
|
12178
|
+
const tasks = this.readIndex();
|
|
12179
|
+
tasks[task.id] = indexEntry(task, key, legacy, signature);
|
|
12180
|
+
this.writeIndex(Object.values(tasks));
|
|
11215
12181
|
} catch {}
|
|
11216
|
-
if (current !== `*
|
|
11217
|
-
`)
|
|
11218
|
-
retryTransient(() => fs6.writeFileSync(this.gitignorePath, `*
|
|
11219
|
-
`));
|
|
11220
12182
|
}
|
|
11221
|
-
|
|
11222
|
-
|
|
12183
|
+
parseTask(record, taskId) {
|
|
12184
|
+
const parsed = this.parseValue(record, taskRecordSchema, "task");
|
|
12185
|
+
if (!parsed.ok)
|
|
12186
|
+
return { ...parsed, details: { ...parsed.details, taskId } };
|
|
12187
|
+
if (parsed.data.id !== taskId)
|
|
12188
|
+
return failure2("recovery_required", "task log and record ID differ", { taskId });
|
|
12189
|
+
return parsed;
|
|
12190
|
+
}
|
|
12191
|
+
parseRecord(bytes, schema, label) {
|
|
12192
|
+
let value;
|
|
11223
12193
|
try {
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
12194
|
+
value = JSON.parse(bytes);
|
|
12195
|
+
} catch {
|
|
12196
|
+
return failure2("recovery_required", `${label} record JSON is corrupt`);
|
|
12197
|
+
}
|
|
12198
|
+
return this.parseValue(value, schema, label);
|
|
12199
|
+
}
|
|
12200
|
+
parseValue(value, schema, label) {
|
|
12201
|
+
if (isObject4(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
|
|
12202
|
+
return failure2("unsupported_version", `unsupported ${label} schema version`);
|
|
12203
|
+
const parsed = parseStoredRecord(schema, value);
|
|
12204
|
+
if (parsed.success)
|
|
12205
|
+
return success(null, null, parsed.data);
|
|
12206
|
+
const writerVersion = isObject4(value) && isObject4(value.runtime) ? value.runtime.updatedWith : null;
|
|
12207
|
+
if (parsed.critical.length > 0)
|
|
12208
|
+
return failure2("recovery_required", `${label} record requires fields this Workit cannot read (${parsed.critical.join(", ")}); upgrade Workit before mutating this checkout`);
|
|
12209
|
+
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
12210
|
+
return failure2("recovery_required", `${label} record was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
12211
|
+
return failure2("recovery_required", `${label} record does not satisfy its schema`);
|
|
12212
|
+
}
|
|
12213
|
+
conflict(expected, actual) {
|
|
12214
|
+
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", { expectedRevision: expected, actualRevision: actual });
|
|
12215
|
+
}
|
|
12216
|
+
workspaceConflict(expected, actual) {
|
|
12217
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
12218
|
+
}
|
|
12219
|
+
v2Dirs() {
|
|
12220
|
+
return [...new Set([this.root, this.origin])].map((dir) => path9.join(dir, ".workit"));
|
|
12221
|
+
}
|
|
12222
|
+
pendingMigrations() {
|
|
12223
|
+
return this.v2Dirs().filter((dir) => {
|
|
12224
|
+
const raw = this.readV2Workspace(path9.join(dir, "workspace.json"));
|
|
12225
|
+
return raw !== null && raw !== TOMBSTONE;
|
|
12226
|
+
});
|
|
12227
|
+
}
|
|
12228
|
+
migrateV2(v2) {
|
|
12229
|
+
const dirs = this.paths();
|
|
12230
|
+
const v2Workspace = path9.join(v2, "workspace.json");
|
|
12231
|
+
const v2Tasks = path9.join(v2, "tasks");
|
|
12232
|
+
const legacy = path9.join(dirs.dir, "legacy", checkoutSlug(path9.dirname(v2)), "v2");
|
|
12233
|
+
const raw = this.readV2Workspace(v2Workspace);
|
|
12234
|
+
if (raw === null || raw === TOMBSTONE)
|
|
12235
|
+
return success(null, null, null);
|
|
12236
|
+
return this.withLock(dirs.checkoutLock, () => this.withLock(path9.join(v2, "metadata.lock"), () => {
|
|
12237
|
+
const workspace = this.readV2Workspace(v2Workspace);
|
|
12238
|
+
if (workspace === null || workspace === TOMBSTONE)
|
|
12239
|
+
return success(null, null, null);
|
|
12240
|
+
const parsedWorkspace = this.parseRecord(workspace.bytes, workspaceRecordSchema, "2.x workspace");
|
|
12241
|
+
if (!parsedWorkspace.ok)
|
|
12242
|
+
return failure2("recovery_required", `cannot migrate the 2.x store at ${v2}: ${parsedWorkspace.error}; move it aside to start fresh`, { path: v2Workspace });
|
|
12243
|
+
let files = [];
|
|
12244
|
+
try {
|
|
12245
|
+
files = fs8.readdirSync(v2Tasks).filter((name) => name.endsWith(".json") && validId(name.slice(0, -5))).filter((name) => !this.isStub(path9.join(v2Tasks, name)));
|
|
12246
|
+
} catch {}
|
|
12247
|
+
const current = this.readWorkspace();
|
|
12248
|
+
if (!current.ok)
|
|
12249
|
+
return current;
|
|
12250
|
+
let workspaceMoved = false;
|
|
12251
|
+
let target = current.data;
|
|
12252
|
+
if (!target) {
|
|
12253
|
+
target = { ...parsedWorkspace.data, root: this.root };
|
|
12254
|
+
const written = this.writeWorkspace(target);
|
|
12255
|
+
if (!written.ok)
|
|
12256
|
+
return written;
|
|
12257
|
+
workspaceMoved = true;
|
|
12258
|
+
}
|
|
12259
|
+
let migrated = 0;
|
|
12260
|
+
let skipped = 0;
|
|
12261
|
+
const done = new Map;
|
|
12262
|
+
try {
|
|
12263
|
+
fs8.mkdirSync(path9.join(legacy, "tasks"), { recursive: true });
|
|
12264
|
+
for (const name of ["workspace.json", "index.json"])
|
|
12265
|
+
if (fs8.existsSync(path9.join(v2, name)))
|
|
12266
|
+
fs8.copyFileSync(path9.join(v2, name), path9.join(legacy, name));
|
|
12267
|
+
for (const name of files) {
|
|
12268
|
+
const id = name.slice(0, -5);
|
|
12269
|
+
const file = path9.join(v2Tasks, name);
|
|
12270
|
+
const bytes = fs8.readFileSync(file, "utf8");
|
|
12271
|
+
const digest = sha256(bytes);
|
|
12272
|
+
fs8.writeFileSync(path9.join(legacy, "tasks", name), bytes);
|
|
12273
|
+
const parsed = this.parseRecord(bytes, taskRecordSchema, "2.x task");
|
|
12274
|
+
if (!parsed.ok || parsed.data.id !== id)
|
|
12275
|
+
return failure2("recovery_required", `cannot migrate 2.x task ${id}: ${parsed.ok ? "file name and record ID differ" : parsed.error}; move ${file} aside to continue`, { path: file });
|
|
12276
|
+
const record = { ...parsed.data, workspaceId: target.id };
|
|
12277
|
+
const origin = { path: path9.join(legacy, "tasks", name), digest };
|
|
12278
|
+
if (!fs8.existsSync(this.eventsPath(id))) {
|
|
12279
|
+
this.openTask(record, "migrated.from_v2", null, origin);
|
|
12280
|
+
migrated += 1;
|
|
12281
|
+
} else {
|
|
12282
|
+
const resynced = this.withLock(this.taskLockPath(id), () => {
|
|
12283
|
+
const loaded = this.load(id);
|
|
12284
|
+
if (!loaded.ok)
|
|
12285
|
+
return loaded;
|
|
12286
|
+
if (!loaded.data)
|
|
12287
|
+
return failure2("not_found", "task not found", { taskId: id });
|
|
12288
|
+
if (loaded.data.legacy?.digest === digest)
|
|
12289
|
+
return success(null, null, false);
|
|
12290
|
+
const appended = this.appendPatch(loaded.data, record, {
|
|
12291
|
+
type: "migrated.from_v2",
|
|
12292
|
+
legacy: origin
|
|
12293
|
+
});
|
|
12294
|
+
return appended.ok ? success(null, null, true) : appended;
|
|
12295
|
+
});
|
|
12296
|
+
if (!resynced.ok)
|
|
12297
|
+
return resynced;
|
|
12298
|
+
if (resynced.data)
|
|
12299
|
+
migrated += 1;
|
|
12300
|
+
else
|
|
12301
|
+
skipped += 1;
|
|
12302
|
+
}
|
|
12303
|
+
done.set(name, digest);
|
|
12304
|
+
}
|
|
12305
|
+
} catch (error) {
|
|
12306
|
+
return failure2("storage_error", `cannot migrate the 2.x store at ${v2}: ${String(error)}`, {
|
|
12307
|
+
path: v2
|
|
12308
|
+
});
|
|
12309
|
+
}
|
|
12310
|
+
try {
|
|
12311
|
+
let complete = true;
|
|
12312
|
+
for (const [name, digest] of done) {
|
|
12313
|
+
const file = path9.join(v2Tasks, name);
|
|
12314
|
+
if (sha256(fs8.readFileSync(file, "utf8")) !== digest) {
|
|
12315
|
+
complete = false;
|
|
12316
|
+
continue;
|
|
12317
|
+
}
|
|
12318
|
+
replaceFile(file, this.stubFor(name.slice(0, -5)), false);
|
|
12319
|
+
}
|
|
12320
|
+
if (!complete)
|
|
12321
|
+
return failure2("busy", `the 2.x store at ${v2} changed during migration; retry`, {
|
|
12322
|
+
path: v2
|
|
12323
|
+
});
|
|
12324
|
+
fs8.rmSync(path9.join(v2, "index.json"), { force: true });
|
|
12325
|
+
this.writeTombstone(v2, target.id);
|
|
12326
|
+
} catch (error) {
|
|
12327
|
+
return failure2("storage_error", `cannot finish the 2.x migration: ${String(error)}`, {
|
|
12328
|
+
path: v2
|
|
12329
|
+
});
|
|
12330
|
+
}
|
|
12331
|
+
reportMigration({
|
|
12332
|
+
from: v2,
|
|
12333
|
+
to: dirs.dir,
|
|
12334
|
+
backup: legacy,
|
|
12335
|
+
tasks: migrated,
|
|
12336
|
+
skipped,
|
|
12337
|
+
workspace: workspaceMoved
|
|
12338
|
+
});
|
|
12339
|
+
return success(null, null, null);
|
|
12340
|
+
}));
|
|
12341
|
+
}
|
|
12342
|
+
adoptLocalStore() {
|
|
12343
|
+
const dirs = this.paths();
|
|
12344
|
+
const marker = path9.join(dirs.v2, "store.json");
|
|
12345
|
+
if (!dirs.shared || !fs8.existsSync(marker))
|
|
12346
|
+
return success(null, null, null);
|
|
12347
|
+
return this.withLock(dirs.checkoutLock, () => {
|
|
12348
|
+
if (!fs8.existsSync(marker))
|
|
12349
|
+
return success(null, null, null);
|
|
12350
|
+
let tasks = 0;
|
|
11232
12351
|
try {
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
12352
|
+
const merge = (from, to) => {
|
|
12353
|
+
if (!fs8.existsSync(to)) {
|
|
12354
|
+
fs8.mkdirSync(path9.dirname(to), { recursive: true });
|
|
12355
|
+
try {
|
|
12356
|
+
fs8.renameSync(from, to);
|
|
12357
|
+
} catch {
|
|
12358
|
+
fs8.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
|
|
12359
|
+
fs8.rmSync(from, { recursive: true, force: true });
|
|
12360
|
+
}
|
|
12361
|
+
return;
|
|
12362
|
+
}
|
|
12363
|
+
if (fs8.lstatSync(from).isDirectory() && fs8.lstatSync(to).isDirectory())
|
|
12364
|
+
for (const name of fs8.readdirSync(from))
|
|
12365
|
+
merge(path9.join(from, name), path9.join(to, name));
|
|
12366
|
+
};
|
|
12367
|
+
for (const sub of ["tasks", "checkouts", "blobs", "legacy"]) {
|
|
12368
|
+
const from = path9.join(dirs.v2, sub);
|
|
12369
|
+
if (!fs8.existsSync(from))
|
|
12370
|
+
continue;
|
|
12371
|
+
if (sub === "tasks") {
|
|
12372
|
+
for (const name of fs8.readdirSync(from))
|
|
12373
|
+
if (validId(name) && !fs8.existsSync(path9.join(dirs.dir, sub, name)))
|
|
12374
|
+
tasks += 1;
|
|
12375
|
+
}
|
|
12376
|
+
merge(from, path9.join(dirs.dir, sub));
|
|
12377
|
+
}
|
|
12378
|
+
const ledger = path9.join(dirs.v2, "ledger");
|
|
12379
|
+
if (fs8.existsSync(ledger) && !fs8.existsSync(path9.join(dirs.dir, "ledger")))
|
|
12380
|
+
merge(ledger, path9.join(dirs.dir, "ledger"));
|
|
12381
|
+
fs8.rmSync(path9.join(dirs.v2, "task-index.json"), { force: true });
|
|
12382
|
+
fs8.renameSync(marker, path9.join(dirs.v2, "store.moved.json"));
|
|
12383
|
+
} catch (error) {
|
|
12384
|
+
return failure2("storage_error", `cannot move ${dirs.v2} into ${dirs.dir}: ${String(error)}`, {
|
|
12385
|
+
path: dirs.v2
|
|
12386
|
+
});
|
|
11237
12387
|
}
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
return failure2("storage_error", `snapshot replacement failed: ${String(error)}`, {
|
|
11246
|
-
path: file
|
|
12388
|
+
reportMigration({
|
|
12389
|
+
from: dirs.v2,
|
|
12390
|
+
to: dirs.dir,
|
|
12391
|
+
backup: dirs.v2,
|
|
12392
|
+
tasks,
|
|
12393
|
+
skipped: 0,
|
|
12394
|
+
workspace: false
|
|
11247
12395
|
});
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
try {
|
|
11251
|
-
fs6.unlinkSync(temporary);
|
|
11252
|
-
} catch {}
|
|
11253
|
-
}
|
|
12396
|
+
return success(null, null, null);
|
|
12397
|
+
});
|
|
11254
12398
|
}
|
|
11255
|
-
|
|
12399
|
+
readV2Workspace(file) {
|
|
12400
|
+
let bytes;
|
|
11256
12401
|
try {
|
|
11257
|
-
|
|
11258
|
-
if (!isObject2(value) || value.version !== INDEX_VERSION || value.workspaceId !== workspaceId || !isObject2(value.tasks))
|
|
11259
|
-
return {};
|
|
11260
|
-
const tasks = {};
|
|
11261
|
-
for (const [id, entry] of Object.entries(value.tasks))
|
|
11262
|
-
if (isObject2(entry) && entry.id === id && typeof entry.file === "string" && typeof entry.status === "string" && typeof entry.updatedAt === "string" && Array.isArray(entry.sessions) && isObject2(entry.progress) && isObject2(entry.source))
|
|
11263
|
-
tasks[id] = entry;
|
|
11264
|
-
return tasks;
|
|
12402
|
+
bytes = fs8.readFileSync(file, "utf8");
|
|
11265
12403
|
} catch {
|
|
11266
|
-
return
|
|
12404
|
+
return null;
|
|
11267
12405
|
}
|
|
12406
|
+
try {
|
|
12407
|
+
const value = JSON.parse(bytes);
|
|
12408
|
+
if (isObject4(value) && isObject4(value.store) && value.store.format === "workit-store")
|
|
12409
|
+
return TOMBSTONE;
|
|
12410
|
+
} catch {}
|
|
12411
|
+
return { bytes };
|
|
12412
|
+
}
|
|
12413
|
+
writeTombstone(v2, workspaceId) {
|
|
12414
|
+
const dirs = this.paths();
|
|
12415
|
+
const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
|
|
12416
|
+
const marker = {
|
|
12417
|
+
schemaVersion: SCHEMA_VERSION,
|
|
12418
|
+
id: workspaceId ?? newId(),
|
|
12419
|
+
revision: newRevision(),
|
|
12420
|
+
root: path9.dirname(v2),
|
|
12421
|
+
runtime: { createdWith: version, updatedWith: version },
|
|
12422
|
+
writer: null,
|
|
12423
|
+
store: {
|
|
12424
|
+
format: "workit-store",
|
|
12425
|
+
version: STORE_FORMAT,
|
|
12426
|
+
path: dirs.dir,
|
|
12427
|
+
note: `workit ${STORE_MIN_RUNTIME}+ keeps task state in ${dirs.dir}; upgrade Workit`
|
|
12428
|
+
},
|
|
12429
|
+
critical: ["store"]
|
|
12430
|
+
};
|
|
12431
|
+
fs8.mkdirSync(v2, { recursive: true });
|
|
12432
|
+
const ignore = path9.join(v2, ".gitignore");
|
|
12433
|
+
if (!fs8.existsSync(ignore))
|
|
12434
|
+
fs8.writeFileSync(ignore, `*
|
|
12435
|
+
`);
|
|
12436
|
+
replaceFile(path9.join(v2, "workspace.json"), `${JSON.stringify(marker, null, 2)}
|
|
12437
|
+
`, true);
|
|
12438
|
+
}
|
|
12439
|
+
stubFor(taskId) {
|
|
12440
|
+
const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
|
|
12441
|
+
return `${JSON.stringify({
|
|
12442
|
+
schemaVersion: SCHEMA_VERSION,
|
|
12443
|
+
id: taskId,
|
|
12444
|
+
runtime: { createdWith: version, updatedWith: version },
|
|
12445
|
+
store: { format: "workit-store", version: STORE_FORMAT, path: this.paths().dir },
|
|
12446
|
+
critical: ["store"]
|
|
12447
|
+
})}
|
|
12448
|
+
`;
|
|
11268
12449
|
}
|
|
11269
|
-
|
|
11270
|
-
const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
12450
|
+
isStub(file) {
|
|
11271
12451
|
try {
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
workspaceId,
|
|
11275
|
-
tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
|
|
11276
|
-
}), { mode: 384, flag: "wx" });
|
|
11277
|
-
retryTransient(() => fs6.renameSync(temporary, this.indexPath));
|
|
12452
|
+
const value = JSON.parse(fs8.readFileSync(file, "utf8"));
|
|
12453
|
+
return isObject4(value) && isObject4(value.store) && value.store.format === "workit-store";
|
|
11278
12454
|
} catch {
|
|
11279
|
-
|
|
11280
|
-
fs6.unlinkSync(temporary);
|
|
11281
|
-
} catch {}
|
|
12455
|
+
return false;
|
|
11282
12456
|
}
|
|
11283
12457
|
}
|
|
11284
|
-
|
|
12458
|
+
writeTombstoneIfMissing() {
|
|
11285
12459
|
try {
|
|
11286
|
-
const
|
|
11287
|
-
if (
|
|
12460
|
+
const v2 = path9.join(this.root, ".workit");
|
|
12461
|
+
if (fs8.existsSync(path9.join(v2, "workspace.json")))
|
|
11288
12462
|
return;
|
|
11289
|
-
|
|
11290
|
-
tasks[task.id] = indexEntry(task, signature);
|
|
11291
|
-
this.writeIndex(task.workspaceId, Object.values(tasks));
|
|
11292
|
-
} catch {}
|
|
11293
|
-
}
|
|
11294
|
-
saveRecovery(file, bytes) {
|
|
11295
|
-
const target = path7.basename(file) === "workspace.json" ? "workspace" : "task";
|
|
11296
|
-
const id = target === "task" ? path7.basename(file, ".json") : "workspace";
|
|
11297
|
-
const destination = path7.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
|
|
11298
|
-
if (fs6.existsSync(destination)) {
|
|
11299
|
-
if (digestBytes(retryTransient(() => fs6.readFileSync(destination))) !== digestBytes(bytes))
|
|
11300
|
-
throw new Error("recovery copy already exists with different bytes");
|
|
11301
|
-
const stamp = new Date;
|
|
11302
|
-
retryTransient(() => fs6.utimesSync(destination, stamp, stamp));
|
|
11303
|
-
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
11304
|
-
return;
|
|
11305
|
-
}
|
|
11306
|
-
let temporary;
|
|
11307
|
-
try {
|
|
11308
|
-
temporary = `${destination}.${process.pid}.${randomUUID2()}.tmp`;
|
|
11309
|
-
const fd = fs6.openSync(temporary, "wx", 384);
|
|
12463
|
+
let id = null;
|
|
11310
12464
|
try {
|
|
11311
|
-
|
|
11312
|
-
|
|
11313
|
-
|
|
11314
|
-
fs6.closeSync(fd);
|
|
11315
|
-
}
|
|
11316
|
-
retryTransient(() => fs6.renameSync(temporary, destination));
|
|
11317
|
-
temporary = undefined;
|
|
11318
|
-
this.fsyncDirectory(this.recoveryDir);
|
|
11319
|
-
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
11320
|
-
} finally {
|
|
11321
|
-
if (temporary)
|
|
11322
|
-
try {
|
|
11323
|
-
fs6.unlinkSync(temporary);
|
|
11324
|
-
} catch {}
|
|
11325
|
-
}
|
|
11326
|
-
}
|
|
11327
|
-
pruneRecovery(prefix, keep) {
|
|
11328
|
-
try {
|
|
11329
|
-
const copies = this.recoveryCopies(prefix);
|
|
11330
|
-
const surplus = this.surplusCopies(copies, path7.basename(keep));
|
|
11331
|
-
for (const copy of surplus)
|
|
11332
|
-
fs6.rmSync(copy.path, { force: true });
|
|
12465
|
+
id = JSON.parse(fs8.readFileSync(this.paths().workspace, "utf8")).id ?? null;
|
|
12466
|
+
} catch {}
|
|
12467
|
+
this.writeTombstone(v2, id);
|
|
11333
12468
|
} catch {}
|
|
11334
12469
|
}
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
const file = path7.join(this.recoveryDir, name);
|
|
12470
|
+
initializeStorage() {
|
|
12471
|
+
const dirs = this.paths();
|
|
12472
|
+
retryTransient(() => fs8.mkdirSync(dirs.checkout, { recursive: true }));
|
|
12473
|
+
retryTransient(() => fs8.mkdirSync(dirs.tasks, { recursive: true }));
|
|
12474
|
+
if (!fs8.existsSync(dirs.marker))
|
|
12475
|
+
replaceFile(dirs.marker, `${JSON.stringify({ format: "workit-store", version: STORE_FORMAT, createdWith: runtimeVersion() })}
|
|
12476
|
+
`, false);
|
|
12477
|
+
if (!dirs.shared) {
|
|
12478
|
+
const ignore = path9.join(dirs.dir, ".gitignore");
|
|
12479
|
+
let current = null;
|
|
11346
12480
|
try {
|
|
11347
|
-
|
|
11348
|
-
if (!stat.isFile())
|
|
11349
|
-
continue;
|
|
11350
|
-
copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
|
|
12481
|
+
current = fs8.readFileSync(ignore, "utf8");
|
|
11351
12482
|
} catch {}
|
|
12483
|
+
if (current !== `*
|
|
12484
|
+
`)
|
|
12485
|
+
retryTransient(() => fs8.writeFileSync(ignore, `*
|
|
12486
|
+
`));
|
|
11352
12487
|
}
|
|
11353
|
-
|
|
11354
|
-
}
|
|
11355
|
-
surplusCopies(copies, keep) {
|
|
11356
|
-
const ordered = copies.toSorted((left, right) => left.name === keep ? -1 : right.name === keep ? 1 : left.mtimeNs === right.mtimeNs ? left.name.localeCompare(right.name) : left.mtimeNs > right.mtimeNs ? -1 : 1);
|
|
11357
|
-
return ordered.slice(RECOVERY_COPIES_PER_RECORD);
|
|
12488
|
+
this.writeTombstoneIfMissing();
|
|
11358
12489
|
}
|
|
11359
|
-
|
|
11360
|
-
const
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
12490
|
+
lockOptions(lockPath) {
|
|
12491
|
+
const inProcess = heldInProcess.has(lockPath);
|
|
12492
|
+
return {
|
|
12493
|
+
lockPath,
|
|
12494
|
+
staleMs: Number.MAX_SAFE_INTEGER,
|
|
12495
|
+
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
12496
|
+
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
12497
|
+
staleRecovery: "remove-if-unchanged",
|
|
12498
|
+
shouldReclaim: ({ payload, nowMs }) => {
|
|
12499
|
+
let ageMs = null;
|
|
12500
|
+
try {
|
|
12501
|
+
ageMs = nowMs - fs8.lstatSync(lockPath).mtimeMs;
|
|
12502
|
+
} catch {}
|
|
12503
|
+
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
12504
|
+
},
|
|
12505
|
+
shouldRemoveStaleLock: () => true,
|
|
12506
|
+
parsePayload: parseMetadataLockOrNull,
|
|
12507
|
+
payload: () => ({
|
|
12508
|
+
pid: process.pid,
|
|
12509
|
+
processStart: processStartOf(process.pid),
|
|
12510
|
+
host: localLockHost(),
|
|
12511
|
+
nonce: randomUUID3()
|
|
12512
|
+
})
|
|
11366
12513
|
};
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
12514
|
+
}
|
|
12515
|
+
acquire(lockPath, options) {
|
|
12516
|
+
clearAbandonedReclaimGuard(lockPath);
|
|
12517
|
+
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
12518
|
+
while (true) {
|
|
11370
12519
|
try {
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
for (const copies of groups.values()) {
|
|
11375
|
-
const surplus = this.surplusCopies(copies);
|
|
11376
|
-
report.recovery.kept += copies.length - surplus.length;
|
|
11377
|
-
for (const copy of surplus) {
|
|
11378
|
-
report.recovery.removed += 1;
|
|
11379
|
-
try {
|
|
11380
|
-
report.recovery.removedBytes += fs6.statSync(copy.path).size;
|
|
11381
|
-
} catch {}
|
|
11382
|
-
if (!dryRun)
|
|
11383
|
-
fs6.rmSync(copy.path, { force: true });
|
|
11384
|
-
}
|
|
11385
|
-
}
|
|
11386
|
-
const nowMs = Date.now();
|
|
11387
|
-
for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
|
|
11388
|
-
if (!fs6.existsSync(directory))
|
|
11389
|
-
continue;
|
|
11390
|
-
for (const name of fs6.readdirSync(directory)) {
|
|
11391
|
-
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
11392
|
-
continue;
|
|
11393
|
-
const file = path7.join(directory, name);
|
|
11394
|
-
const stat = fs6.lstatSync(file);
|
|
11395
|
-
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
11396
|
-
continue;
|
|
11397
|
-
report.temporary.removed += 1;
|
|
11398
|
-
if (!dryRun)
|
|
11399
|
-
fs6.rmSync(file, { force: true });
|
|
11400
|
-
}
|
|
11401
|
-
}
|
|
11402
|
-
return success(null, null, null);
|
|
11403
|
-
} catch (error) {
|
|
11404
|
-
return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
|
|
11405
|
-
path: this.recoveryDir
|
|
12520
|
+
return acquireFileLockSync(lockPath, {
|
|
12521
|
+
...options,
|
|
12522
|
+
timeoutMs: Math.max(0, deadline - Date.now())
|
|
11406
12523
|
});
|
|
12524
|
+
} catch (error) {
|
|
12525
|
+
const code = error?.code;
|
|
12526
|
+
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
12527
|
+
throw error;
|
|
12528
|
+
if (code !== "file_lock_stale" && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
|
|
12529
|
+
throw error;
|
|
12530
|
+
if (code !== "file_lock_stale")
|
|
12531
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
11407
12532
|
}
|
|
11408
|
-
};
|
|
11409
|
-
const pruned = dryRun ? sweep() : this.withLock(sweep);
|
|
11410
|
-
if (!pruned.ok)
|
|
11411
|
-
return pruned;
|
|
11412
|
-
const tasks = this.listTasks();
|
|
11413
|
-
if (!tasks.ok)
|
|
11414
|
-
return tasks;
|
|
11415
|
-
for (const task of tasks.data) {
|
|
11416
|
-
const deduped = dedupeCandidates(task.candidates);
|
|
11417
|
-
const removed = task.candidates.length - deduped.length;
|
|
11418
|
-
if (removed === 0)
|
|
11419
|
-
continue;
|
|
11420
|
-
if (task.status === "active") {
|
|
11421
|
-
report.candidates.skippedActive.push(task.id);
|
|
11422
|
-
continue;
|
|
11423
|
-
}
|
|
11424
|
-
if (task.status === "closed") {
|
|
11425
|
-
report.candidates.skippedClosed.push(task.id);
|
|
11426
|
-
continue;
|
|
11427
|
-
}
|
|
11428
|
-
if (!dryRun) {
|
|
11429
|
-
const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
|
|
11430
|
-
...current,
|
|
11431
|
-
candidates: dedupeCandidates(current.candidates)
|
|
11432
|
-
}));
|
|
11433
|
-
if (!written.ok) {
|
|
11434
|
-
report.candidates.failed.push(task.id);
|
|
11435
|
-
continue;
|
|
11436
|
-
}
|
|
11437
|
-
}
|
|
11438
|
-
report.candidates.removed += removed;
|
|
11439
|
-
report.candidates.tasks.push(task.id);
|
|
11440
12533
|
}
|
|
11441
|
-
return success(null, null, report);
|
|
11442
12534
|
}
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
return
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
return null;
|
|
11452
|
-
for (const candidate of candidates.data) {
|
|
11453
|
-
if (candidate.target !== target || candidate.digest !== digest)
|
|
11454
|
-
continue;
|
|
11455
|
-
try {
|
|
11456
|
-
if (!fs6.lstatSync(candidate.path).isFile())
|
|
11457
|
-
continue;
|
|
11458
|
-
const bytes = fs6.readFileSync(candidate.path);
|
|
11459
|
-
if (digestBytes(bytes) !== digest)
|
|
11460
|
-
continue;
|
|
11461
|
-
if (target === "workspace") {
|
|
11462
|
-
const parsed = this.parseBytes(bytes, workspaceRecordSchema);
|
|
11463
|
-
if (parsed.ok && parsed.data.id === workspaceId && parsed.data.root === this.root)
|
|
11464
|
-
return bytes;
|
|
11465
|
-
} else {
|
|
11466
|
-
const parsed = this.parseBytes(bytes, taskRecordSchema);
|
|
11467
|
-
if (parsed.ok && parsed.data.id === taskId && parsed.data.workspaceId === workspace?.id)
|
|
11468
|
-
return bytes;
|
|
11469
|
-
}
|
|
11470
|
-
} catch {}
|
|
12535
|
+
withLock(lockPath, operation) {
|
|
12536
|
+
try {
|
|
12537
|
+
this.initializeStorage();
|
|
12538
|
+
fs8.mkdirSync(path9.dirname(lockPath), { recursive: true });
|
|
12539
|
+
} catch (error) {
|
|
12540
|
+
return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
|
|
12541
|
+
path: this.paths().dir
|
|
12542
|
+
});
|
|
11471
12543
|
}
|
|
11472
|
-
|
|
11473
|
-
}
|
|
11474
|
-
fsyncDirectory(directory) {
|
|
12544
|
+
let handle;
|
|
11475
12545
|
try {
|
|
11476
|
-
|
|
11477
|
-
try {
|
|
11478
|
-
fs6.fsyncSync(fd);
|
|
11479
|
-
} finally {
|
|
11480
|
-
fs6.closeSync(fd);
|
|
11481
|
-
}
|
|
12546
|
+
handle = this.acquire(lockPath, this.lockOptions(lockPath));
|
|
11482
12547
|
} catch (error) {
|
|
11483
|
-
|
|
11484
|
-
throw error;
|
|
12548
|
+
return this.lockFailure(lockPath, error);
|
|
11485
12549
|
}
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
if (!fs6.existsSync(this.tasksDir))
|
|
11489
|
-
return null;
|
|
11490
|
-
const ids = new Set;
|
|
12550
|
+
hold(lockPath);
|
|
12551
|
+
let result;
|
|
11491
12552
|
try {
|
|
11492
|
-
|
|
11493
|
-
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
12553
|
+
if (!handle.verifyStillHeld())
|
|
12554
|
+
result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
|
|
12555
|
+
else {
|
|
12556
|
+
try {
|
|
12557
|
+
result = operation();
|
|
12558
|
+
} catch (error) {
|
|
12559
|
+
result = failure2("storage_error", `mutation failed: ${String(error)}`);
|
|
12560
|
+
}
|
|
11499
12561
|
}
|
|
11500
|
-
} catch {
|
|
11501
|
-
|
|
12562
|
+
} catch (error) {
|
|
12563
|
+
result = this.lockFailure(lockPath, error);
|
|
12564
|
+
}
|
|
12565
|
+
drop(lockPath);
|
|
12566
|
+
try {
|
|
12567
|
+
retryTransient(() => handle.release());
|
|
12568
|
+
} catch (error) {
|
|
12569
|
+
const released = this.lockFailure(lockPath, error);
|
|
12570
|
+
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
12571
|
+
result = result.ok ? released : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
11502
12572
|
}
|
|
11503
|
-
return
|
|
12573
|
+
return result;
|
|
11504
12574
|
}
|
|
11505
|
-
|
|
12575
|
+
lockDirWritable(lockPath) {
|
|
12576
|
+
const probe = `${lockPath}.${process.pid}.${randomUUID3()}.probe`;
|
|
11506
12577
|
try {
|
|
11507
|
-
|
|
12578
|
+
fs8.closeSync(fs8.openSync(probe, "wx", 384));
|
|
11508
12579
|
} catch {
|
|
11509
|
-
return
|
|
12580
|
+
return false;
|
|
11510
12581
|
}
|
|
12582
|
+
try {
|
|
12583
|
+
fs8.rmSync(probe, { force: true });
|
|
12584
|
+
} catch {}
|
|
12585
|
+
return true;
|
|
11511
12586
|
}
|
|
11512
|
-
|
|
12587
|
+
lockHolderPresent(lockPath) {
|
|
11513
12588
|
try {
|
|
11514
|
-
|
|
11515
|
-
return
|
|
12589
|
+
fs8.lstatSync(lockPath);
|
|
12590
|
+
return true;
|
|
11516
12591
|
} catch (error) {
|
|
11517
|
-
|
|
11518
|
-
return { exists: false, result: success(null, null, null) };
|
|
11519
|
-
return {
|
|
11520
|
-
exists: true,
|
|
11521
|
-
result: failure2("recovery_required", `snapshot cannot be read: ${String(error)}`, {
|
|
11522
|
-
path: file
|
|
11523
|
-
})
|
|
11524
|
-
};
|
|
12592
|
+
return error?.code !== "ENOENT";
|
|
11525
12593
|
}
|
|
11526
12594
|
}
|
|
11527
|
-
|
|
11528
|
-
|
|
11529
|
-
|
|
11530
|
-
|
|
11531
|
-
|
|
11532
|
-
|
|
12595
|
+
lockFailure(lockPath, error) {
|
|
12596
|
+
const value = error;
|
|
12597
|
+
const code = typeof value?.code === "string" ? value.code : "";
|
|
12598
|
+
if (isTransientWindowsError(error) && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
|
|
12599
|
+
return failure2("storage_error", `cannot create the workit lock (${code}); no other Workit call holds it`, {
|
|
12600
|
+
path: lockPath,
|
|
12601
|
+
guidance: `Check permissions and the read-only attribute on ${path9.dirname(lockPath)}.`
|
|
12602
|
+
});
|
|
12603
|
+
if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
|
|
12604
|
+
let lock = null;
|
|
12605
|
+
try {
|
|
12606
|
+
lock = parseMetadataLockOrNull(fs8.readFileSync(lockPath, "utf8"));
|
|
12607
|
+
} catch {}
|
|
12608
|
+
if (lock?.externalAction)
|
|
12609
|
+
return failure2("writer_conflict", "workspace is reserved by a managed external action", {
|
|
12610
|
+
outcome: "not_started",
|
|
12611
|
+
path: lockPath
|
|
12612
|
+
});
|
|
12613
|
+
return failure2("busy", `workit lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
|
|
12614
|
+
outcome: "not_started",
|
|
12615
|
+
path: lockPath,
|
|
12616
|
+
guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
|
|
12617
|
+
});
|
|
11533
12618
|
}
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
const parsed = parseStoredRecord(schema, value);
|
|
11537
|
-
if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
|
|
11538
|
-
return success(null, null, parsed.data);
|
|
11539
|
-
const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
|
|
11540
|
-
const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
|
|
11541
|
-
if (parsed.success)
|
|
11542
|
-
return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
|
|
11543
|
-
if (parsed.critical.length > 0)
|
|
11544
|
-
return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
|
|
11545
|
-
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
11546
|
-
return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
11547
|
-
return failure2("recovery_required", "snapshot does not satisfy its schema");
|
|
11548
|
-
}
|
|
11549
|
-
conflict(expected, actual) {
|
|
11550
|
-
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
|
|
11551
|
-
expectedRevision: expected,
|
|
11552
|
-
actualRevision: actual
|
|
11553
|
-
});
|
|
11554
|
-
}
|
|
11555
|
-
workspaceConflict(expected, actual) {
|
|
11556
|
-
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
11557
|
-
}
|
|
11558
|
-
taskPath(taskId) {
|
|
11559
|
-
return path7.join(this.tasksDir, `${taskId}.json`);
|
|
11560
|
-
}
|
|
11561
|
-
get workitDir() {
|
|
11562
|
-
return path7.join(this.root, ".workit");
|
|
11563
|
-
}
|
|
11564
|
-
get tasksDir() {
|
|
11565
|
-
return path7.join(this.workitDir, "tasks");
|
|
11566
|
-
}
|
|
11567
|
-
get recoveryDir() {
|
|
11568
|
-
return path7.join(this.workitDir, "recovery");
|
|
11569
|
-
}
|
|
11570
|
-
get workspacePath() {
|
|
11571
|
-
return path7.join(this.workitDir, "workspace.json");
|
|
11572
|
-
}
|
|
11573
|
-
get indexPath() {
|
|
11574
|
-
return path7.join(this.workitDir, "index.json");
|
|
11575
|
-
}
|
|
11576
|
-
get lockPath() {
|
|
11577
|
-
return path7.join(this.workitDir, "metadata.lock");
|
|
11578
|
-
}
|
|
11579
|
-
get gitignorePath() {
|
|
11580
|
-
return path7.join(this.workitDir, ".gitignore");
|
|
12619
|
+
const recovery = code === "metadata_lock_invalid" || code === "not-file" || /metadata lock|file lock|reclaim/i.test(String(value?.message ?? error));
|
|
12620
|
+
return failure2(recovery ? "recovery_required" : "storage_error", `lock operation failed: ${String(error)}`, { path: lockPath });
|
|
11581
12621
|
}
|
|
11582
12622
|
}
|
|
11583
12623
|
|
|
@@ -11654,7 +12694,7 @@ var operationAutoClass = (operation) => typeof operation === "string" ? OPERATIO
|
|
|
11654
12694
|
var digestOf = (configDirPath) => {
|
|
11655
12695
|
try {
|
|
11656
12696
|
const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
|
|
11657
|
-
return
|
|
12697
|
+
return createHash7("sha256").update(raw).digest("hex");
|
|
11658
12698
|
} catch {
|
|
11659
12699
|
return null;
|
|
11660
12700
|
}
|
|
@@ -11698,7 +12738,7 @@ var targetOf = (root, descriptor) => {
|
|
|
11698
12738
|
const cwd = parsed?.payload?.cwd;
|
|
11699
12739
|
if (cwd === undefined)
|
|
11700
12740
|
return root;
|
|
11701
|
-
if (typeof cwd !== "string" || !
|
|
12741
|
+
if (typeof cwd !== "string" || !path10.isAbsolute(cwd))
|
|
11702
12742
|
return null;
|
|
11703
12743
|
return realpathSync3(cwd);
|
|
11704
12744
|
} catch {
|
|
@@ -11923,11 +12963,11 @@ function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
|
11923
12963
|
return [];
|
|
11924
12964
|
return task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map((entry) => entry.data);
|
|
11925
12965
|
}
|
|
11926
|
-
var
|
|
12966
|
+
var digestBytes = (bytes) => createHash8("sha256").update(bytes).digest("hex");
|
|
11927
12967
|
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
11928
12968
|
let root;
|
|
11929
12969
|
try {
|
|
11930
|
-
root =
|
|
12970
|
+
root = fs9.realpathSync(checkoutRoot);
|
|
11931
12971
|
} catch {
|
|
11932
12972
|
return failure2("permission_denied", "checkout root is unavailable");
|
|
11933
12973
|
}
|
|
@@ -11936,23 +12976,23 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
11936
12976
|
continue;
|
|
11937
12977
|
if (!reference.digest)
|
|
11938
12978
|
return failure2("invalid_input", "document references require a byte digest");
|
|
11939
|
-
const target =
|
|
11940
|
-
if (target !== root && !target.startsWith(`${root}${
|
|
12979
|
+
const target = path11.resolve(root, reference.path);
|
|
12980
|
+
if (target !== root && !target.startsWith(`${root}${path11.sep}`))
|
|
11941
12981
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
11942
12982
|
fields: [
|
|
11943
12983
|
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
11944
12984
|
]
|
|
11945
12985
|
});
|
|
11946
12986
|
try {
|
|
11947
|
-
const relative =
|
|
12987
|
+
const relative = path11.relative(root, target);
|
|
11948
12988
|
let current = root;
|
|
11949
|
-
for (const segment of relative.split(
|
|
12989
|
+
for (const segment of relative.split(path11.sep)) {
|
|
11950
12990
|
if (!segment)
|
|
11951
12991
|
continue;
|
|
11952
|
-
current =
|
|
11953
|
-
const stat =
|
|
11954
|
-
const real =
|
|
11955
|
-
if (real !== root && !real.startsWith(`${root}${
|
|
12992
|
+
current = path11.join(current, segment);
|
|
12993
|
+
const stat = fs9.lstatSync(current);
|
|
12994
|
+
const real = fs9.realpathSync(current);
|
|
12995
|
+
if (real !== root && !real.startsWith(`${root}${path11.sep}`))
|
|
11956
12996
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
11957
12997
|
fields: [
|
|
11958
12998
|
{
|
|
@@ -11968,7 +13008,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
11968
13008
|
if (current !== target && !stat.isDirectory())
|
|
11969
13009
|
return failure2("invalid_input", "document reference ancestor is not a directory");
|
|
11970
13010
|
}
|
|
11971
|
-
if (
|
|
13011
|
+
if (digestBytes(fs9.readFileSync(target)) !== reference.digest)
|
|
11972
13012
|
return failure2("permission_denied", "approved document bytes have changed");
|
|
11973
13013
|
} catch {
|
|
11974
13014
|
return failure2("invalid_input", "approved document is unavailable");
|
|
@@ -12332,9 +13372,9 @@ var bindingCovers = scopeCovers;
|
|
|
12332
13372
|
var verifyDecisionContent = verifyContentRefs;
|
|
12333
13373
|
|
|
12334
13374
|
// packages/workit-core/src/check-config.ts
|
|
12335
|
-
import { spawnSync as
|
|
12336
|
-
import
|
|
12337
|
-
import
|
|
13375
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
13376
|
+
import fs10 from "node:fs";
|
|
13377
|
+
import path12 from "node:path";
|
|
12338
13378
|
var SCRIPT_CHECKS = ["test", "lint", "typecheck", "check"];
|
|
12339
13379
|
var CHECKS_FILE = "workit.checks.json";
|
|
12340
13380
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -12376,7 +13416,7 @@ function splitCommand(command) {
|
|
|
12376
13416
|
return argv.length ? argv : new Error("empty command");
|
|
12377
13417
|
}
|
|
12378
13418
|
var git = (cwd, args) => {
|
|
12379
|
-
const run =
|
|
13419
|
+
const run = spawnSync4("git", args, {
|
|
12380
13420
|
cwd,
|
|
12381
13421
|
encoding: "utf8",
|
|
12382
13422
|
timeout: 20000,
|
|
@@ -12387,10 +13427,10 @@ var git = (cwd, args) => {
|
|
|
12387
13427
|
return value || null;
|
|
12388
13428
|
};
|
|
12389
13429
|
function checkRoot(cwd) {
|
|
12390
|
-
return
|
|
13430
|
+
return path12.resolve(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
|
|
12391
13431
|
}
|
|
12392
13432
|
var packageManager = (root) => {
|
|
12393
|
-
const has = (file) =>
|
|
13433
|
+
const has = (file) => fs10.existsSync(path12.join(root, file));
|
|
12394
13434
|
if (has("bun.lock") || has("bun.lockb"))
|
|
12395
13435
|
return "bun";
|
|
12396
13436
|
if (has("pnpm-lock.yaml"))
|
|
@@ -12410,7 +13450,7 @@ var CHECK_NAME = /^[a-z0-9][a-z0-9:._-]{0,63}$/u;
|
|
|
12410
13450
|
function detectedChecks(root) {
|
|
12411
13451
|
const read = (file) => {
|
|
12412
13452
|
try {
|
|
12413
|
-
return
|
|
13453
|
+
return fs10.readFileSync(path12.join(root, file), "utf8");
|
|
12414
13454
|
} catch {
|
|
12415
13455
|
return null;
|
|
12416
13456
|
}
|
|
@@ -12449,7 +13489,7 @@ function detectedChecks(root) {
|
|
|
12449
13489
|
var GATES = ["testing", "verification"];
|
|
12450
13490
|
function loadCheckConfig(cwd) {
|
|
12451
13491
|
const root = checkRoot(cwd);
|
|
12452
|
-
const file =
|
|
13492
|
+
const file = path12.join(root, CHECKS_FILE);
|
|
12453
13493
|
const none = (error = null) => ({
|
|
12454
13494
|
source: error ? CHECKS_FILE : "none",
|
|
12455
13495
|
root,
|
|
@@ -12459,7 +13499,7 @@ function loadCheckConfig(cwd) {
|
|
|
12459
13499
|
});
|
|
12460
13500
|
let text = null;
|
|
12461
13501
|
try {
|
|
12462
|
-
text =
|
|
13502
|
+
text = fs10.readFileSync(file, "utf8");
|
|
12463
13503
|
} catch (error) {
|
|
12464
13504
|
if (error.code !== "ENOENT")
|
|
12465
13505
|
return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
|
|
@@ -12522,10 +13562,10 @@ function workitAvailable(env = process.env) {
|
|
|
12522
13562
|
return workitOnPath;
|
|
12523
13563
|
const windows = process.platform === "win32";
|
|
12524
13564
|
const exts = windows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
12525
|
-
const dirs = (env.PATH ?? env.Path ?? "").split(
|
|
13565
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path12.delimiter).filter(Boolean);
|
|
12526
13566
|
workitOnPath = dirs.some((dir) => exts.some((ext) => {
|
|
12527
13567
|
try {
|
|
12528
|
-
return
|
|
13568
|
+
return fs10.statSync(path12.join(dir, `workit${ext}`)).isFile();
|
|
12529
13569
|
} catch {
|
|
12530
13570
|
return false;
|
|
12531
13571
|
}
|
|
@@ -12537,15 +13577,15 @@ function checkCommand(args, version, onPath = workitAvailable()) {
|
|
|
12537
13577
|
}
|
|
12538
13578
|
|
|
12539
13579
|
// packages/workit-core/src/git/rev.ts
|
|
12540
|
-
import { spawnSync as
|
|
12541
|
-
import { createHash as
|
|
12542
|
-
import
|
|
13580
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
13581
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
13582
|
+
import fs11 from "node:fs";
|
|
12543
13583
|
import os2 from "node:os";
|
|
12544
|
-
import
|
|
13584
|
+
import path13 from "node:path";
|
|
12545
13585
|
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
12546
13586
|
var GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
|
|
12547
13587
|
var git2 = (cwd, args, options = {}) => {
|
|
12548
|
-
const result =
|
|
13588
|
+
const result = spawnSync5("git", args, {
|
|
12549
13589
|
cwd,
|
|
12550
13590
|
encoding: "utf8",
|
|
12551
13591
|
env: options.env ?? process.env,
|
|
@@ -12575,10 +13615,10 @@ function headSha(cwd) {
|
|
|
12575
13615
|
var emptyTree = (cwd) => line(git2(cwd, ["mktree"], { input: "" }));
|
|
12576
13616
|
var MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
|
|
12577
13617
|
var seedIndex = (from, to) => {
|
|
12578
|
-
|
|
12579
|
-
const { atimeNs, mtimeNs } =
|
|
13618
|
+
fs11.copyFileSync(from, to);
|
|
13619
|
+
const { atimeNs, mtimeNs } = fs11.statSync(from, { bigint: true });
|
|
12580
13620
|
const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
|
|
12581
|
-
|
|
13621
|
+
fs11.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
|
|
12582
13622
|
};
|
|
12583
13623
|
function worktreeTree(cwd, options = {}) {
|
|
12584
13624
|
const top = line(git2(cwd, ["rev-parse", "--show-toplevel"]));
|
|
@@ -12587,10 +13627,10 @@ function worktreeTree(cwd, options = {}) {
|
|
|
12587
13627
|
return null;
|
|
12588
13628
|
const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
|
|
12589
13629
|
const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
|
|
12590
|
-
const scratch =
|
|
13630
|
+
const scratch = fs11.mkdtempSync(path13.join(os2.tmpdir(), "workit-tree-"));
|
|
12591
13631
|
try {
|
|
12592
|
-
const tempIndex =
|
|
12593
|
-
if (
|
|
13632
|
+
const tempIndex = path13.join(scratch, "index");
|
|
13633
|
+
if (fs11.existsSync(indexPath))
|
|
12594
13634
|
seedIndex(indexPath, tempIndex);
|
|
12595
13635
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
12596
13636
|
const untracked = git2(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
@@ -12602,7 +13642,7 @@ function worktreeTree(cwd, options = {}) {
|
|
|
12602
13642
|
const skipped = [];
|
|
12603
13643
|
for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
|
|
12604
13644
|
try {
|
|
12605
|
-
const stat =
|
|
13645
|
+
const stat = fs11.statSync(path13.join(top, file));
|
|
12606
13646
|
if (stat.isFile() && stat.size > cap)
|
|
12607
13647
|
skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
|
|
12608
13648
|
} catch {}
|
|
@@ -12619,10 +13659,10 @@ function worktreeTree(cwd, options = {}) {
|
|
|
12619
13659
|
if (!tree)
|
|
12620
13660
|
return null;
|
|
12621
13661
|
const base = line(git2(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
|
|
12622
|
-
const key = skipped.length ? `sha256:${
|
|
13662
|
+
const key = skipped.length ? `sha256:${createHash9("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
|
|
12623
13663
|
return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
|
|
12624
13664
|
} finally {
|
|
12625
|
-
|
|
13665
|
+
fs11.rmSync(scratch, { recursive: true, force: true });
|
|
12626
13666
|
}
|
|
12627
13667
|
}
|
|
12628
13668
|
function worktreeSignal(cwd, options = {}) {
|
|
@@ -12642,16 +13682,16 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
12642
13682
|
});
|
|
12643
13683
|
if (!status.ok)
|
|
12644
13684
|
return null;
|
|
12645
|
-
const hash =
|
|
13685
|
+
const hash = createHash9("sha256");
|
|
12646
13686
|
hash.update(`${headSha(top) ?? "unborn"}\x00`);
|
|
12647
13687
|
for (const entry of status.stdout.split("\x00")) {
|
|
12648
13688
|
if (entry.length < 4)
|
|
12649
13689
|
continue;
|
|
12650
13690
|
const file = entry.slice(3);
|
|
12651
|
-
const absolute =
|
|
13691
|
+
const absolute = path13.join(top, file);
|
|
12652
13692
|
let stat = "-";
|
|
12653
13693
|
try {
|
|
12654
|
-
const info =
|
|
13694
|
+
const info = fs11.lstatSync(absolute, { bigint: true });
|
|
12655
13695
|
stat = `${info.mode.toString(8)}:${info.size}:${info.mtimeNs}`;
|
|
12656
13696
|
if (info.isDirectory())
|
|
12657
13697
|
stat += `:${headSha(absolute) ?? "-"}`;
|
|
@@ -12662,15 +13702,15 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
12662
13702
|
}
|
|
12663
13703
|
|
|
12664
13704
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
12665
|
-
var
|
|
12666
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${
|
|
12667
|
-
var relative = (root, candidate) =>
|
|
13705
|
+
var digestBytes2 = (value) => createHash10("sha256").update(value).digest("hex");
|
|
13706
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path14.sep}`);
|
|
13707
|
+
var relative = (root, candidate) => path14.relative(root, candidate).split(path14.sep).join("/") || ".";
|
|
12668
13708
|
var canonicalPath = (value) => {
|
|
12669
13709
|
if (typeof value !== "string" || !value)
|
|
12670
13710
|
return null;
|
|
12671
13711
|
if (value.includes("\\") || value.split("/").includes(".."))
|
|
12672
13712
|
return null;
|
|
12673
|
-
const normalized =
|
|
13713
|
+
const normalized = path14.posix.normalize(value);
|
|
12674
13714
|
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
12675
13715
|
return null;
|
|
12676
13716
|
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
@@ -12706,7 +13746,7 @@ var compareCodeUnits2 = (left, right) => {
|
|
|
12706
13746
|
return left.length - right.length;
|
|
12707
13747
|
};
|
|
12708
13748
|
var gitPaths = (root) => {
|
|
12709
|
-
const result =
|
|
13749
|
+
const result = spawnSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
12710
13750
|
cwd: root,
|
|
12711
13751
|
encoding: "buffer"
|
|
12712
13752
|
});
|
|
@@ -12718,21 +13758,21 @@ var gitPaths = (root) => {
|
|
|
12718
13758
|
git: !/not a git repository/i.test(stderr)
|
|
12719
13759
|
};
|
|
12720
13760
|
}
|
|
12721
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
12722
|
-
const stagedDeleted =
|
|
13761
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")) : [];
|
|
13762
|
+
const stagedDeleted = spawnSync6("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
12723
13763
|
if (stagedDeleted.status !== 0) {
|
|
12724
13764
|
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
12725
13765
|
if (!/not a git repository/i.test(stderr))
|
|
12726
13766
|
return { paths, uncertain: true, git: true };
|
|
12727
13767
|
} else if (stagedDeleted.stdout) {
|
|
12728
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
13768
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")));
|
|
12729
13769
|
}
|
|
12730
13770
|
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
12731
13771
|
};
|
|
12732
13772
|
var walk = (root, directory, output) => {
|
|
12733
13773
|
let names;
|
|
12734
13774
|
try {
|
|
12735
|
-
names =
|
|
13775
|
+
names = fs12.readdirSync(directory);
|
|
12736
13776
|
} catch {
|
|
12737
13777
|
return true;
|
|
12738
13778
|
}
|
|
@@ -12740,10 +13780,10 @@ var walk = (root, directory, output) => {
|
|
|
12740
13780
|
for (const name of names) {
|
|
12741
13781
|
if (name === ".git" || name === ".workit")
|
|
12742
13782
|
continue;
|
|
12743
|
-
const target =
|
|
13783
|
+
const target = path14.join(directory, name);
|
|
12744
13784
|
let stat;
|
|
12745
13785
|
try {
|
|
12746
|
-
stat =
|
|
13786
|
+
stat = fs12.lstatSync(target);
|
|
12747
13787
|
} catch {
|
|
12748
13788
|
uncertain = true;
|
|
12749
13789
|
continue;
|
|
@@ -12758,14 +13798,14 @@ var walk = (root, directory, output) => {
|
|
|
12758
13798
|
var scopeRoots = (root, scope) => {
|
|
12759
13799
|
const roots = [];
|
|
12760
13800
|
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
12761
|
-
const target =
|
|
13801
|
+
const target = path14.resolve(root, item);
|
|
12762
13802
|
if (!inside(root, target))
|
|
12763
13803
|
return failure2("invalid_input", `candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] });
|
|
12764
13804
|
let ancestor = target;
|
|
12765
|
-
while (!
|
|
12766
|
-
ancestor =
|
|
13805
|
+
while (!fs12.existsSync(ancestor) && ancestor !== root)
|
|
13806
|
+
ancestor = path14.dirname(ancestor);
|
|
12767
13807
|
try {
|
|
12768
|
-
if (!inside(root,
|
|
13808
|
+
if (!inside(root, fs12.realpathSync(ancestor)))
|
|
12769
13809
|
return failure2("invalid_input", `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] });
|
|
12770
13810
|
} catch {
|
|
12771
13811
|
return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
@@ -12777,7 +13817,7 @@ var scopeRoots = (root, scope) => {
|
|
|
12777
13817
|
function captureCandidate(root, scope, environment = []) {
|
|
12778
13818
|
let checkout;
|
|
12779
13819
|
try {
|
|
12780
|
-
checkout =
|
|
13820
|
+
checkout = fs12.realpathSync(root);
|
|
12781
13821
|
} catch {
|
|
12782
13822
|
return failure2("invalid_input", "candidate root does not exist");
|
|
12783
13823
|
}
|
|
@@ -12795,11 +13835,11 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12795
13835
|
if (item === ".git" || item === ".workit")
|
|
12796
13836
|
continue;
|
|
12797
13837
|
names.add(item);
|
|
12798
|
-
if (!
|
|
13838
|
+
if (!fs12.existsSync(target))
|
|
12799
13839
|
continue;
|
|
12800
13840
|
let stat;
|
|
12801
13841
|
try {
|
|
12802
|
-
stat =
|
|
13842
|
+
stat = fs12.lstatSync(target);
|
|
12803
13843
|
} catch {
|
|
12804
13844
|
uncertain = true;
|
|
12805
13845
|
continue;
|
|
@@ -12815,17 +13855,17 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12815
13855
|
for (const item of [...names].toSorted()) {
|
|
12816
13856
|
if (!scopeMatches(item, normalizedScope))
|
|
12817
13857
|
continue;
|
|
12818
|
-
const target =
|
|
13858
|
+
const target = path14.join(checkout, item);
|
|
12819
13859
|
try {
|
|
12820
|
-
const stat =
|
|
13860
|
+
const stat = fs12.lstatSync(target);
|
|
12821
13861
|
if (stat.isSymbolicLink()) {
|
|
12822
|
-
const link =
|
|
12823
|
-
files.push({ path: item, kind: "symlink", digest:
|
|
13862
|
+
const link = fs12.readlinkSync(target);
|
|
13863
|
+
files.push({ path: item, kind: "symlink", digest: digestBytes2(link), executable: null });
|
|
12824
13864
|
} else if (stat.isFile()) {
|
|
12825
13865
|
files.push({
|
|
12826
13866
|
path: item,
|
|
12827
13867
|
kind: "file",
|
|
12828
|
-
digest:
|
|
13868
|
+
digest: digestBytes2(fs12.readFileSync(target)),
|
|
12829
13869
|
executable: (stat.mode & 73) !== 0
|
|
12830
13870
|
});
|
|
12831
13871
|
} else if (!stat.isDirectory()) {
|
|
@@ -12850,7 +13890,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12850
13890
|
const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
12851
13891
|
if (values.some(({ value }) => value === null))
|
|
12852
13892
|
completeness = "uncertain";
|
|
12853
|
-
const headResult =
|
|
13893
|
+
const headResult = spawnSync6("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
12854
13894
|
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
12855
13895
|
completeness = "uncertain";
|
|
12856
13896
|
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
@@ -13863,6 +14903,38 @@ var applyWorkerLifecycle = (input) => {
|
|
|
13863
14903
|
return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during lifecycle observation");
|
|
13864
14904
|
};
|
|
13865
14905
|
var refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] }));
|
|
14906
|
+
var TASK_SCOPED_ACTIONS = new Set([
|
|
14907
|
+
"task.inspect",
|
|
14908
|
+
"task.revise",
|
|
14909
|
+
"task.progress",
|
|
14910
|
+
"task.pause",
|
|
14911
|
+
"task.resume",
|
|
14912
|
+
"task.close",
|
|
14913
|
+
"policy.assess",
|
|
14914
|
+
"policy.preview",
|
|
14915
|
+
"policy.explain",
|
|
14916
|
+
"evidence.record",
|
|
14917
|
+
"finding.record",
|
|
14918
|
+
"finding.resolve",
|
|
14919
|
+
"decision.record",
|
|
14920
|
+
"decision.revoke",
|
|
14921
|
+
"worker.assign",
|
|
14922
|
+
"worker.report",
|
|
14923
|
+
"worker.cancel",
|
|
14924
|
+
"writer.acquire",
|
|
14925
|
+
"writer.release",
|
|
14926
|
+
"state.export"
|
|
14927
|
+
]);
|
|
14928
|
+
var CREATING_ACTIONS = new Set([
|
|
14929
|
+
"task.revise",
|
|
14930
|
+
"task.progress",
|
|
14931
|
+
"policy.assess",
|
|
14932
|
+
"evidence.record",
|
|
14933
|
+
"finding.record",
|
|
14934
|
+
"decision.record",
|
|
14935
|
+
"worker.assign",
|
|
14936
|
+
"writer.acquire"
|
|
14937
|
+
]);
|
|
13866
14938
|
var REVISION_RETRY_ATTEMPTS = 8;
|
|
13867
14939
|
var retryPause = new Int32Array(new SharedArrayBuffer(4));
|
|
13868
14940
|
var filledRevisions = (request) => {
|
|
@@ -14114,7 +15186,9 @@ class WorkitCore {
|
|
|
14114
15186
|
}
|
|
14115
15187
|
contextRootError() {
|
|
14116
15188
|
try {
|
|
14117
|
-
|
|
15189
|
+
realpathSync6(this.context.root);
|
|
15190
|
+
const root = checkoutRootOf(this.context.root);
|
|
15191
|
+
if (root !== this.store.root && !sameDirectoryIdentity(root, this.store.root))
|
|
14118
15192
|
return failure2("invalid_input", "operation context root does not match the task store root");
|
|
14119
15193
|
} catch {
|
|
14120
15194
|
return failure2("invalid_input", "operation context root cannot be resolved");
|
|
@@ -14136,6 +15210,30 @@ class WorkitCore {
|
|
|
14136
15210
|
retryOmittedRevisions(request, run) {
|
|
14137
15211
|
return retryFilledRevisions(filledRevisions(request), run);
|
|
14138
15212
|
}
|
|
15213
|
+
implicitTask(family, request) {
|
|
15214
|
+
if (typeof request !== "object" || request === null || Array.isArray(request))
|
|
15215
|
+
return success(null, null, request);
|
|
15216
|
+
const value = request;
|
|
15217
|
+
if (value.taskId !== undefined || typeof value.action !== "string")
|
|
15218
|
+
return success(null, null, request);
|
|
15219
|
+
const action = `${family}.${value.action}`;
|
|
15220
|
+
if (!TASK_SCOPED_ACTIONS.has(action))
|
|
15221
|
+
return success(null, null, request);
|
|
15222
|
+
const root = this.contextRootError();
|
|
15223
|
+
if (!root.ok)
|
|
15224
|
+
return root;
|
|
15225
|
+
const create = CREATING_ACTIONS.has(action) && (this.context.workerId ?? null) === null;
|
|
15226
|
+
const found = this.store.implicitTask({
|
|
15227
|
+
provenance: provenance(this.context),
|
|
15228
|
+
create,
|
|
15229
|
+
now: trustedNow(this.context)
|
|
15230
|
+
});
|
|
15231
|
+
if (!found.ok)
|
|
15232
|
+
return found;
|
|
15233
|
+
if (!found.data)
|
|
15234
|
+
return failure2("not_found", 'no task for this branch yet; pass taskId, or record something first (`workit task start "<objective>"`, a note, a check) to create it');
|
|
15235
|
+
return success(null, null, { ...request, taskId: found.data.task.id });
|
|
15236
|
+
}
|
|
14139
15237
|
helperEntry(task, requireSession = false, requireActive = false) {
|
|
14140
15238
|
const workerId = this.context.workerId ?? null;
|
|
14141
15239
|
if (workerId === null)
|
|
@@ -14167,7 +15265,11 @@ class WorkitCore {
|
|
|
14167
15265
|
return null;
|
|
14168
15266
|
}
|
|
14169
15267
|
state(request) {
|
|
14170
|
-
|
|
15268
|
+
const resolved = this.implicitTask("state", request);
|
|
15269
|
+
if (!resolved.ok)
|
|
15270
|
+
return resolved;
|
|
15271
|
+
const value = resolved.data;
|
|
15272
|
+
return value?.action === "import" ? this.retryOmittedRevisions(value, () => this.stateOnce(value)) : this.stateOnce(value);
|
|
14171
15273
|
}
|
|
14172
15274
|
stateOnce(request) {
|
|
14173
15275
|
const root = this.contextRootError();
|
|
@@ -14234,44 +15336,14 @@ class WorkitCore {
|
|
|
14234
15336
|
return imported;
|
|
14235
15337
|
return this.summary(imported.data);
|
|
14236
15338
|
}
|
|
14237
|
-
|
|
14238
|
-
return failure2("permission_denied", "native recovery authority is unavailable");
|
|
14239
|
-
const processEvidence = (lock, writer) => this.context.nativeRecovery({
|
|
14240
|
-
lock,
|
|
14241
|
-
writer,
|
|
14242
|
-
reason: input.reason,
|
|
14243
|
-
authorityRefs: input.authorityRefs
|
|
14244
|
-
});
|
|
14245
|
-
if (input.target === "workspace")
|
|
14246
|
-
return this.store.recoverWorkspace({
|
|
14247
|
-
expectedBytes: input.expectedBytes,
|
|
14248
|
-
snapshotDigest: input.snapshotDigest,
|
|
14249
|
-
reason: input.reason,
|
|
14250
|
-
authorityRefs: input.authorityRefs,
|
|
14251
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
14252
|
-
processEvidence
|
|
14253
|
-
});
|
|
14254
|
-
const workspace = this.store.readWorkspace();
|
|
14255
|
-
if (!workspace.ok)
|
|
14256
|
-
return workspace;
|
|
14257
|
-
if (!workspace.data)
|
|
14258
|
-
return failure2("not_found", "workspace not found");
|
|
14259
|
-
this.fillRevisions(input, undefined, workspace.data);
|
|
14260
|
-
if (input.target === "workspace" && input.taskId !== undefined)
|
|
14261
|
-
return failure2("invalid_input", "workspace recovery takes no task ID");
|
|
14262
|
-
if (input.target === "task" && input.taskId === undefined)
|
|
14263
|
-
return failure2("invalid_input", "task recovery requires a task ID");
|
|
14264
|
-
return this.store.recoverTask(input.taskId, {
|
|
14265
|
-
expectedBytes: input.expectedBytes,
|
|
14266
|
-
snapshotDigest: input.snapshotDigest,
|
|
14267
|
-
reason: input.reason,
|
|
14268
|
-
authorityRefs: input.authorityRefs,
|
|
14269
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
14270
|
-
processEvidence
|
|
14271
|
-
});
|
|
15339
|
+
return failure2("invalid_input", "unknown state action");
|
|
14272
15340
|
}
|
|
14273
15341
|
task(request) {
|
|
14274
|
-
|
|
15342
|
+
const resolved = this.implicitTask("task", request);
|
|
15343
|
+
if (!resolved.ok)
|
|
15344
|
+
return resolved;
|
|
15345
|
+
const value = resolved.data;
|
|
15346
|
+
return this.retryOmittedRevisions(value, () => this.taskOnce(value));
|
|
14275
15347
|
}
|
|
14276
15348
|
taskOnce(request) {
|
|
14277
15349
|
const root = this.contextRootError();
|
|
@@ -14373,7 +15445,11 @@ class WorkitCore {
|
|
|
14373
15445
|
}
|
|
14374
15446
|
}
|
|
14375
15447
|
policy(request) {
|
|
14376
|
-
|
|
15448
|
+
const resolved = this.implicitTask("policy", request);
|
|
15449
|
+
if (!resolved.ok)
|
|
15450
|
+
return resolved;
|
|
15451
|
+
const value = resolved.data;
|
|
15452
|
+
return this.retryOmittedRevisions(value, () => this.policyOnce(value));
|
|
14377
15453
|
}
|
|
14378
15454
|
policyOnce(request) {
|
|
14379
15455
|
const root = this.contextRootError();
|
|
@@ -14422,7 +15498,11 @@ class WorkitCore {
|
|
|
14422
15498
|
return success(changed.data.revision, null, changed.data.policy);
|
|
14423
15499
|
}
|
|
14424
15500
|
evidence(request) {
|
|
14425
|
-
|
|
15501
|
+
const resolved = this.implicitTask("evidence", request);
|
|
15502
|
+
if (!resolved.ok)
|
|
15503
|
+
return resolved;
|
|
15504
|
+
const value = resolved.data;
|
|
15505
|
+
return this.retryOmittedRevisions(value, () => this.evidenceOnce(value));
|
|
14426
15506
|
}
|
|
14427
15507
|
evidenceOnce(request) {
|
|
14428
15508
|
const root = this.contextRootError();
|
|
@@ -14563,13 +15643,16 @@ class WorkitCore {
|
|
|
14563
15643
|
return success(changed.data.revision, null, changed.data.evidence.at(-1));
|
|
14564
15644
|
}
|
|
14565
15645
|
decision(request) {
|
|
14566
|
-
|
|
15646
|
+
const resolved = this.implicitTask("decision", request);
|
|
15647
|
+
return resolved.ok ? this.recordDecision(resolved.data) : resolved;
|
|
14567
15648
|
}
|
|
14568
15649
|
observeDecision(request, observation) {
|
|
14569
|
-
|
|
15650
|
+
const resolved = this.implicitTask("decision", request);
|
|
15651
|
+
return resolved.ok ? this.recordDecision(resolved.data, observation, true) : resolved;
|
|
14570
15652
|
}
|
|
14571
15653
|
observeStandingDecision(request) {
|
|
14572
|
-
|
|
15654
|
+
const resolved = this.implicitTask("decision", request);
|
|
15655
|
+
return resolved.ok ? this.recordDecision(resolved.data, undefined, true, true) : resolved;
|
|
14573
15656
|
}
|
|
14574
15657
|
commitTask(taskId, expected, filled, recheck, update) {
|
|
14575
15658
|
let revision = expected;
|
|
@@ -14731,7 +15814,11 @@ class WorkitCore {
|
|
|
14731
15814
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "decision disappeared");
|
|
14732
15815
|
}
|
|
14733
15816
|
finding(request) {
|
|
14734
|
-
|
|
15817
|
+
const resolved = this.implicitTask("finding", request);
|
|
15818
|
+
if (!resolved.ok)
|
|
15819
|
+
return resolved;
|
|
15820
|
+
const value = resolved.data;
|
|
15821
|
+
return this.retryOmittedRevisions(value, () => this.findingOnce(value));
|
|
14735
15822
|
}
|
|
14736
15823
|
findingOnce(request) {
|
|
14737
15824
|
const root = this.contextRootError();
|
|
@@ -14857,7 +15944,11 @@ class WorkitCore {
|
|
|
14857
15944
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "finding disappeared");
|
|
14858
15945
|
}
|
|
14859
15946
|
worker(request) {
|
|
14860
|
-
|
|
15947
|
+
const resolved = this.implicitTask("worker", request);
|
|
15948
|
+
if (!resolved.ok)
|
|
15949
|
+
return resolved;
|
|
15950
|
+
const value = resolved.data;
|
|
15951
|
+
return this.retryOmittedRevisions(value, () => this.workerOnce(value));
|
|
14861
15952
|
}
|
|
14862
15953
|
workerOnce(request) {
|
|
14863
15954
|
const root = this.contextRootError();
|
|
@@ -15207,7 +16298,11 @@ class WorkitCore {
|
|
|
15207
16298
|
return applied;
|
|
15208
16299
|
}
|
|
15209
16300
|
writer(request) {
|
|
15210
|
-
|
|
16301
|
+
const resolved = this.implicitTask("writer", request);
|
|
16302
|
+
if (!resolved.ok)
|
|
16303
|
+
return resolved;
|
|
16304
|
+
const value = resolved.data;
|
|
16305
|
+
return this.retryOmittedRevisions(value, () => this.writerOnce(value));
|
|
15211
16306
|
}
|
|
15212
16307
|
writerOnce(request) {
|
|
15213
16308
|
const root = this.contextRootError();
|
|
@@ -15817,6 +16912,8 @@ class WorkitCore {
|
|
|
15817
16912
|
}
|
|
15818
16913
|
|
|
15819
16914
|
// packages/workit-core/src/hooks/context.ts
|
|
16915
|
+
var MIGRATION_PENDING_NOTE = "workit migration pending — run `workit task status`";
|
|
16916
|
+
var migrationPending = (result) => !result.ok && result.code === "needs_input" && (result.error ?? "").startsWith(MIGRATION_PENDING);
|
|
15820
16917
|
var boundTo = (entry, session) => entry.sessions.some((item) => item.host === session.host && item.handle === session.handle);
|
|
15821
16918
|
var newestFirst = (left, right) => right.updatedAt.localeCompare(left.updatedAt);
|
|
15822
16919
|
var sessionTaskEntry = (entries, session) => entries.filter((entry) => entry.status !== "closed" && boundTo(entry, session)).toSorted(newestFirst)[0] ?? null;
|
|
@@ -15827,6 +16924,15 @@ var currentTaskEntry = (entries, session, selection) => {
|
|
|
15827
16924
|
const active = entries.filter((entry) => entry.status === "active");
|
|
15828
16925
|
return active.length === 1 ? active[0] : null;
|
|
15829
16926
|
};
|
|
16927
|
+
var implicitTaskEntry = (store, entries, session) => {
|
|
16928
|
+
const open = entries.filter((entry) => entry.key !== null && entry.status !== "closed" && !entry.sessions.some((item) => item.host !== "workit_cli" && !(item.host === session.host && item.handle === session.handle)));
|
|
16929
|
+
if (open.length === 0)
|
|
16930
|
+
return null;
|
|
16931
|
+
const key = store.currentKey();
|
|
16932
|
+
if (!key.ok)
|
|
16933
|
+
return null;
|
|
16934
|
+
return open.filter((entry) => entry.key === key.data.key).toSorted(newestFirst)[0] ?? null;
|
|
16935
|
+
};
|
|
15830
16936
|
var unfinishedTaskOffer = (entries, session, excludeTaskId = null) => {
|
|
15831
16937
|
const tasks = unboundOpenTaskEntries(entries.filter((entry) => entry.id !== excludeTaskId), session);
|
|
15832
16938
|
if (tasks.length === 0)
|
|
@@ -15858,8 +16964,8 @@ var cache = new Map;
|
|
|
15858
16964
|
function sessionCompactContext(store, session, context, selection = "session-bound") {
|
|
15859
16965
|
const listed = store.listTaskIndex();
|
|
15860
16966
|
if (!listed.ok)
|
|
15861
|
-
return null;
|
|
15862
|
-
const entry = currentTaskEntry(listed.data, session, selection);
|
|
16967
|
+
return migrationPending(listed) ? MIGRATION_PENDING_NOTE : null;
|
|
16968
|
+
const entry = currentTaskEntry(listed.data, session, selection) ?? implicitTaskEntry(store, listed.data, session);
|
|
15863
16969
|
return entry ? entryCompactContext(store, entry, session, context) : null;
|
|
15864
16970
|
}
|
|
15865
16971
|
function entryCompactContext(store, entry, session, context) {
|
|
@@ -15884,7 +16990,7 @@ function entryCompactContext(store, entry, session, context) {
|
|
|
15884
16990
|
return null;
|
|
15885
16991
|
const freshness = { mode: "signal", current: () => recentSignal(store.root) };
|
|
15886
16992
|
const observed = task.data.evidence.some(isObservedCheck);
|
|
15887
|
-
const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [
|
|
16993
|
+
const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [path15.resolve(store.root, ref.path)] : []));
|
|
15888
16994
|
const observedFiles = filesKey(files);
|
|
15889
16995
|
const compact = new WorkitCore(store, context).compactContext(entry.id, freshness);
|
|
15890
16996
|
if (!compact.ok)
|
|
@@ -15917,9 +17023,15 @@ var sessionContextText = (input, descriptor, options) => {
|
|
|
15917
17023
|
try {
|
|
15918
17024
|
const store = new TaskStore(input.cwd);
|
|
15919
17025
|
const listed = store.listTaskIndex();
|
|
17026
|
+
if (migrationPending(listed))
|
|
17027
|
+
return `<workit-contract>
|
|
17028
|
+
${invariantBootstrap()}
|
|
17029
|
+
${MIGRATION_PENDING_NOTE}${options.addendum ? `
|
|
17030
|
+
${options.addendum}` : ""}
|
|
17031
|
+
</workit-contract>`;
|
|
15920
17032
|
if (!listed.ok)
|
|
15921
17033
|
throw new Error(listed.error);
|
|
15922
|
-
const entry = currentTaskEntry(listed.data, session, descriptor.context.task);
|
|
17034
|
+
const entry = currentTaskEntry(listed.data, session, descriptor.context.task) ?? implicitTaskEntry(store, listed.data, session);
|
|
15923
17035
|
const text = entry ? entryCompactContext(store, entry, session, hookOperationContext(input, descriptor)) : null;
|
|
15924
17036
|
if (text)
|
|
15925
17037
|
compact = `
|
|
@@ -15939,6 +17051,8 @@ ${options.addendum}` : ""}
|
|
|
15939
17051
|
var turnContextText = (input, descriptor) => {
|
|
15940
17052
|
try {
|
|
15941
17053
|
const text = sessionCompactContext(new TaskStore(input.cwd), { host: input.host, handle: input.session.id }, hookOperationContext(input, descriptor), descriptor.context.task);
|
|
17054
|
+
if (text === MIGRATION_PENDING_NOTE)
|
|
17055
|
+
return text;
|
|
15942
17056
|
return text ? `<workit-task-context>${text}</workit-task-context>` : null;
|
|
15943
17057
|
} catch {
|
|
15944
17058
|
return null;
|
|
@@ -15970,7 +17084,16 @@ var usable2 = (value) => value === "native" || value === "partial";
|
|
|
15970
17084
|
var NONE2 = { kind: "none" };
|
|
15971
17085
|
var offered = new Set;
|
|
15972
17086
|
var CLAUDE_WORKTREE_IMPLEMENTER = "workit:implementer";
|
|
15973
|
-
var
|
|
17087
|
+
var CLAUDE_JUDGES = new Set(["workit:verifier", "workit:reviewer"]);
|
|
17088
|
+
var subagentSession = (session, agentId) => [session, agentId].filter((part) => Boolean(part)).join(":").replace(/[^A-Za-z0-9_.:@/+-]/g, "-").slice(0, 128);
|
|
17089
|
+
var subagentStartText = (host, session, descriptor, event) => {
|
|
17090
|
+
if (host === "claude_code" && event.agentType === CLAUDE_WORKTREE_IMPLEMENTER)
|
|
17091
|
+
return `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) working in its own git worktree: it may edit and commit there, within its brief's scope. Before the first commit, switch to a policy-compliant branch with \`workit git branch <branch> --base <base>\` (e.g. feature/<slug>); branch policy hooks still deny protected or non-compliant branches. Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
|
|
17092
|
+
const readOnly = `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) as read-only/agent-guided; writer delegation is unavailable.`;
|
|
17093
|
+
if (host === "claude_code" && event.agentType && CLAUDE_JUDGES.has(event.agentType) && event.agentId)
|
|
17094
|
+
return `${readOnly} Its own Workit session is ${subagentSession(session, event.agentId)}: record verdicts with \`workit ledger verdict <result> --session ${subagentSession(session, event.agentId)} ...\` so the ledger tells it apart from the author.`;
|
|
17095
|
+
return readOnly;
|
|
17096
|
+
};
|
|
15974
17097
|
function handleHook(input, deps) {
|
|
15975
17098
|
const { descriptor } = deps;
|
|
15976
17099
|
const event = input.event;
|
|
@@ -15996,7 +17119,10 @@ function handleHook(input, deps) {
|
|
|
15996
17119
|
case "shell.pre":
|
|
15997
17120
|
return usable2(descriptor.shellPolicy.deny) ? shellPolicy(input.cwd, event.command) : NONE2;
|
|
15998
17121
|
case "subagent.start":
|
|
15999
|
-
return {
|
|
17122
|
+
return {
|
|
17123
|
+
kind: "context",
|
|
17124
|
+
text: subagentStartText(input.host, input.session.id ?? null, descriptor, event)
|
|
17125
|
+
};
|
|
16000
17126
|
case "compact.pre":
|
|
16001
17127
|
return {
|
|
16002
17128
|
kind: "notice",
|
|
@@ -16041,12 +17167,12 @@ var dispatchHook = (adapter, raw, env = process.env) => {
|
|
|
16041
17167
|
};
|
|
16042
17168
|
// packages/workit-core/src/hooks/hosts/fields.ts
|
|
16043
17169
|
import { existsSync as existsSync5, realpathSync as realpathSync7, statSync as statSync2 } from "node:fs";
|
|
16044
|
-
import
|
|
17170
|
+
import path16 from "node:path";
|
|
16045
17171
|
var isRecord3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16046
17172
|
var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
|
|
16047
17173
|
var optionalText = (value) => typeof value === "string" ? value : null;
|
|
16048
17174
|
var existingDirectory = (value) => {
|
|
16049
|
-
if (!nonEmpty2(value) || !
|
|
17175
|
+
if (!nonEmpty2(value) || !path16.isAbsolute(value) || !existsSync5(value))
|
|
16050
17176
|
return null;
|
|
16051
17177
|
try {
|
|
16052
17178
|
return statSync2(value).isDirectory() ? realpathSync7(value) : null;
|
|
@@ -16550,11 +17676,11 @@ var codexAdapter = {
|
|
|
16550
17676
|
};
|
|
16551
17677
|
},
|
|
16552
17678
|
render: render2,
|
|
16553
|
-
addendum: (input) => `<workit-codex-mutations>Codex MCP is read-only: unattested callers cannot mutate. Run the workit CLI
|
|
17679
|
+
addendum: (input) => `<workit-codex-mutations>Codex MCP is read-only: unattested callers cannot mutate. Run workit verbs with the workit CLI on the shell (node_modules/.bin/workit, or npx -y @brainervirus/workit-cli): check, git branch|commit|push, pr, ci, stack, ledger, handoff; task-family mutations take --json --confirm${input.session.id ? `; bind the writer to this session with node_modules/.bin/workit writer acquire --task <id> --revision <rev> --actor ${input.session.id} --confirm` : ""}. A verifier or reviewer records workit ledger verdict under a session the lead assigns (WORKIT_SESSION_ID=<lead>-v<n>), never the author's. Merge and release need a workspace grant; a question answer is not host permission.</workit-codex-mutations>`
|
|
16554
17680
|
};
|
|
16555
17681
|
|
|
16556
17682
|
// packages/workit-core/src/hooks/hosts/cursor.ts
|
|
16557
|
-
import
|
|
17683
|
+
import path17 from "node:path";
|
|
16558
17684
|
var undocumented2 = { support: "undocumented", native: null };
|
|
16559
17685
|
var none = { support: "none", native: null };
|
|
16560
17686
|
var CURSOR_DESCRIPTOR = {
|
|
@@ -16737,7 +17863,7 @@ var protocolEvent2 = (input) => {
|
|
|
16737
17863
|
return { kind: "compact.pre", trigger: "auto" };
|
|
16738
17864
|
}
|
|
16739
17865
|
};
|
|
16740
|
-
var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(
|
|
17866
|
+
var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path17.sep) ? root : `${root}${path17.sep}`);
|
|
16741
17867
|
var hookCwd = (input) => {
|
|
16742
17868
|
const cwd = existingDirectory(input.cwd);
|
|
16743
17869
|
if (cwd && input.hook_event_name === "beforeShellExecution")
|