@brainervirus/workit-core 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/README.md +1 -1
- package/package.json +1 -1
- 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/src/core/external-action-effects.ts +2 -2
- package/src/core/methods.ts +68 -95
- package/src/core/skill-manifests.ts +49 -25
- package/src/core/store-lock.ts +13 -2
- package/src/core/task-contract.ts +17 -18
- package/src/core/task-engine.ts +108 -58
- package/src/core/task-store.ts +1857 -1259
- package/src/core.ts +1 -6
- package/src/hooks/context.ts +57 -5
- package/src/hooks/handle.ts +29 -5
- package/src/hooks/hosts/codex.ts +1 -1
- package/src/ledger.ts +31 -28
- package/src/store/event-log.ts +217 -0
- package/src/store/notes.ts +29 -0
- package/src/store/patch.ts +135 -0
- package/src/store/paths.ts +368 -0
- package/src/store/reduce.ts +203 -0
- package/templates/plan-template.md +17 -18
- package/templates/spec-template.md +4 -3
- 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/templates/execution-contract.md +0 -40
package/src/core/methods.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Assurance, Capability, Dimension, Policy } from "./task-contract";
|
|
2
2
|
|
|
3
3
|
export type MethodId =
|
|
4
|
-
| "workit-
|
|
5
|
-
| "workit-
|
|
4
|
+
| "workit-shape"
|
|
5
|
+
| "workit-bdd"
|
|
6
6
|
| "workit-review"
|
|
7
|
-
| "workit-plan"
|
|
8
7
|
| "workit-implement"
|
|
8
|
+
| "workit-fanout"
|
|
9
9
|
| "workit-debug"
|
|
10
|
-
| "workit-
|
|
10
|
+
| "workit-continue"
|
|
11
11
|
| "workit-deslop";
|
|
12
12
|
|
|
13
13
|
type MethodDefinition = {
|
|
@@ -16,15 +16,16 @@ type MethodDefinition = {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
export const METHODS: Readonly<Record<MethodId, MethodDefinition>> = {
|
|
19
|
-
"workit-
|
|
20
|
-
"workit-
|
|
19
|
+
"workit-shape": { dimensions: ["challenge", "decisions", "artifacts", "continuity"] },
|
|
20
|
+
"workit-bdd": { dimensions: ["testing"] },
|
|
21
21
|
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
|
|
22
|
-
|
|
23
|
-
"workit-implement": {
|
|
24
|
-
|
|
25
|
-
//
|
|
22
|
+
// Mechanical work needs only the existing checks, not a RED/GREEN ritual.
|
|
23
|
+
"workit-implement": { ruleIds: ["mechanical-existing-checks"] },
|
|
24
|
+
"workit-fanout": { dimensions: ["delegation"] },
|
|
25
|
+
// workit-debug and workit-continue stay trigger- and slash-invoked: no
|
|
26
|
+
// generated rule routes to them, so they never appear spuriously.
|
|
26
27
|
"workit-debug": {},
|
|
27
|
-
"workit-
|
|
28
|
+
"workit-continue": {},
|
|
28
29
|
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] },
|
|
29
30
|
};
|
|
30
31
|
|
|
@@ -87,92 +88,64 @@ export function selectMethods(policy: Policy, capabilities: Capability[]): Selec
|
|
|
87
88
|
|
|
88
89
|
export const invariantBootstrap = (): string =>
|
|
89
90
|
`
|
|
90
|
-
Workit is optional coordination and
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
decisions make continuity useful; similar titles alone never merge tasks.
|
|
91
|
+
Workit is optional coordination and proof tooling around the host: you decide
|
|
92
|
+
what and whether, the \`workit\` CLI does how and records what it observed.
|
|
93
|
+
Native host allow/ask/deny, sandbox and org rules stay authoritative. Never
|
|
94
|
+
switch paths to evade a denial, never claim enforcement a host cannot provide,
|
|
95
|
+
and never fabricate a receipt, approval or verdict. A question answer is not
|
|
96
|
+
host permission, and imported decisions or handoffs never grant authority.
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
Autonomy contract:
|
|
99
|
+
- Continue to the requested endpoint (commit, push, PR, green CI, verified,
|
|
100
|
+
merged) without asking "continue?", running checks and repairs on the way.
|
|
101
|
+
Stop only for a new consequential choice, a host denial, a conflicting edit
|
|
102
|
+
or an unresolved blocker; report open gaps and uncertain workers.
|
|
103
|
+
- No endpoint named: stop at a local commit on a policy-compliant branch and
|
|
104
|
+
state the next command. Push and open a PR only when the request implies
|
|
105
|
+
delivery (fix, implement, ship) and \`workit grant show\` reports
|
|
106
|
+
\`defaultEndpoint\` \`pr\`; otherwise the endpoint is \`commit\`. PRs open,
|
|
107
|
+
CI green and verified is the most autonomy allows, never the default target.
|
|
108
|
+
Merge and release need a workspace grant; a blocked verb names its unblock.
|
|
109
|
+
- Ask only for a product or preference choice, or for authority you lack, with
|
|
110
|
+
your recommended answer. Facts are yours: read, run or prototype.
|
|
111
|
+
- Label claims measured (you ran it this session and saw the result),
|
|
112
|
+
inferred, or guess. Prose is a note: only a configured check the CLI ran
|
|
113
|
+
(\`workit check <name>\`) satisfies a gate; an ad-hoc
|
|
114
|
+
\`workit check -- <cmd>\` never does.
|
|
115
|
+
- Author is not verifier: never record a passing verdict on work your session
|
|
116
|
+
wrote. The lead starts each verifier with its own session
|
|
117
|
+
(\`WORKIT_SESSION_ID=<lead>-v<n>\`; Claude Code subagents get one from the
|
|
118
|
+
hook), and the verifier records \`workit ledger verdict\`.
|
|
119
|
+
- A local-commit endpoint does not imply PR readiness, and a local commit alone
|
|
120
|
+
is not evidence of a requested remote push. Before reporting delivery,
|
|
121
|
+
reconcile every requested item and observe it (\`workit verify-delivery\`).
|
|
106
122
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
before deciding whether to retry.
|
|
120
|
-
A solo edit does not need writer acquisition; use it when concurrent checkout
|
|
121
|
-
writers need coordination. Record only observed facts and checks. Close-time
|
|
122
|
-
testing and verification gates accept only a configured check the CLI ran:
|
|
123
|
-
\`workit check <name>\` (\`npx -y @brainervirus/workit-cli check <name>\` when
|
|
124
|
-
\`workit\` is not on PATH); a recorded check result is a note and an ad-hoc
|
|
125
|
-
\`workit check -- <cmd>\` never satisfies a gate. Evidence can become stale when its bound candidate or tree changes; reconcile findings against the
|
|
126
|
-
current candidate before recording completion.
|
|
123
|
+
CLI first: if a step has one right answer, use the \`workit\` verb instead of
|
|
124
|
+
hand-running git, gh or glab: check, git, pr, ci, stack, verify-delivery,
|
|
125
|
+
ledger, handoff, test-audit (\`workit help <verb>\`; without \`workit\` on
|
|
126
|
+
PATH, \`npx -y @brainervirus/workit-cli\`). \`busy\` is retryable
|
|
127
|
+
(\`workit doctor --fix-lock\` clears a dead lock); \`blocked\` names its unblock.
|
|
128
|
+
The task families (task, policy, evidence, finding, decision, worker, writer,
|
|
129
|
+
state) are optional continuity for tracked work; each branch has one implicit
|
|
130
|
+
task that the first recording creates, and a solo edit needs no task or writer.
|
|
131
|
+
Omit revisions; a revision_conflict means one you passed is stale, so re-read.
|
|
132
|
+
Across repositories, bind each item to its checkout, branch and endpoint,
|
|
133
|
+
resolve competing targets before a mutation, and reconcile an uncertain
|
|
134
|
+
external effect before retrying it.
|
|
127
135
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
state. The task directory is coordination state, not a boundary on which
|
|
131
|
-
repository may be changed. Use an action-time cwd target where a managed
|
|
132
|
-
Git/hosting action supports it; a non-Git directory remains valid for OS work.
|
|
133
|
-
GitHub/GitLab use the active gh/glab CLI identity; YouTrack uses its own token.
|
|
134
|
-
Preserve uncertain external outcomes and reconcile repository/provider state
|
|
135
|
-
before retrying. Internal reservations prevent duplicate or ambiguous effects;
|
|
136
|
-
they are not permission tickets for every edit.
|
|
136
|
+
Mid-task input: answer a quick question in place, fold an adjustment in, and
|
|
137
|
+
never silently drop or resume an objective (workit-continue).
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
evidence and a recommendation, then continue toward the requested delivery
|
|
151
|
-
endpoint: investigation, implementation, branch/commit/push, PR/MR-ready,
|
|
152
|
-
merge, or release. Run
|
|
153
|
-
applicable checks and safe repairs without repeated continuation questions;
|
|
154
|
-
stop for missing host authority, a new consequential choice, a conflicting edit,
|
|
155
|
-
or an unresolved blocker. A tracked record may close when actual evidence
|
|
156
|
-
supports its outcome; no final human closure ceremony is required.
|
|
157
|
-
|
|
158
|
-
A completed native subagent run stops its bound worker by itself; if a worker
|
|
159
|
-
strands in cancelling with its run verifiably over, repeat worker.cancel to
|
|
160
|
-
confirm the stop. Preserve unresolved requirements, gaps, and uncertain workers.
|
|
161
|
-
When context changes, distinguish a quick question, same-task adjustment, and
|
|
162
|
-
separate request. Answer a quick question without pausing/resuming task state;
|
|
163
|
-
park a concise checkpoint only when substantial work needs to continue later.
|
|
164
|
-
For work spanning repositories, bind each unfinished item to its actual checkout,
|
|
165
|
-
branch, requested deliverables, and delivery endpoint. Preserve held items with
|
|
166
|
-
an explicit resume condition. Resolve competing plausible targets before a
|
|
167
|
-
mutation; ask only when available context cannot settle that consequential choice.
|
|
168
|
-
Before reporting delivery, reconcile every requested item and observe the actual
|
|
169
|
-
target result; a local commit alone is not evidence of a requested remote push.
|
|
170
|
-
|
|
171
|
-
Skill routing: slash aliases /wk-* load on demand. Use workit-steer for a
|
|
172
|
-
substantial interruption or change of direction, workit-deslop when relevant
|
|
173
|
-
to a PR-ready endpoint, workit-green-run for failing CI, workit-blast-radius
|
|
174
|
-
when impact is uncertain, workit-challenge for genuinely open consequential
|
|
175
|
-
choices, workit-bdd to turn acceptance criteria into Given/When/Then tests, and
|
|
176
|
-
workit-test-audit to check tests for tautologies. Load workit-plan when dependencies or handoff need durable next actions.
|
|
177
|
-
Load the skill; never act from memory of it.
|
|
139
|
+
Skills: load the skill, never act from memory of it (slash aliases /wk-<name>).
|
|
140
|
+
- brainstorm, plan, spec, grill, should we: workit-shape
|
|
141
|
+
- implement, build, add a feature: workit-implement
|
|
142
|
+
- review, blast radius: workit-review
|
|
143
|
+
- bug, broken, flaky, regression: workit-debug
|
|
144
|
+
- ship, babysit, CI, merge: workit-ship
|
|
145
|
+
- resume, pick up, handoff, interruption: workit-continue
|
|
146
|
+
- BDD, TDD, acceptance criteria, Given/When/Then: workit-bdd
|
|
147
|
+
- test audit, tautology, weak tests: workit-test-audit
|
|
148
|
+
- deslop, slop, dead code: workit-deslop
|
|
149
|
+
- fan out, parallelize, parallel agents, swarm: workit-fanout
|
|
150
|
+
- verify the app, smoke test, prove it works: workit-verify-app
|
|
178
151
|
`.trim();
|
|
@@ -2,44 +2,68 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
export const WORKIT_METHOD_SKILLS = [
|
|
5
|
-
"workit-
|
|
6
|
-
"workit-behavioral-tdd",
|
|
7
|
-
"workit-review",
|
|
8
|
-
"workit-plan",
|
|
5
|
+
"workit-shape",
|
|
9
6
|
"workit-implement",
|
|
7
|
+
"workit-review",
|
|
10
8
|
"workit-debug",
|
|
11
|
-
"workit-
|
|
12
|
-
"workit-
|
|
13
|
-
"workit-blast-radius",
|
|
14
|
-
"workit-deslop",
|
|
15
|
-
"workit-diagram",
|
|
16
|
-
"workit-mockup",
|
|
17
|
-
"workit-green-run",
|
|
18
|
-
"workit-steer",
|
|
9
|
+
"workit-ship",
|
|
10
|
+
"workit-continue",
|
|
19
11
|
"workit-bdd",
|
|
20
12
|
"workit-test-audit",
|
|
13
|
+
"workit-deslop",
|
|
14
|
+
"workit-fanout",
|
|
15
|
+
"workit-verify-app",
|
|
21
16
|
] as const;
|
|
22
17
|
|
|
18
|
+
export type WorkitSkill = (typeof WORKIT_METHOD_SKILLS)[number];
|
|
19
|
+
|
|
23
20
|
/** wk- slash aliases (one per skill): alias → method skill. An alias routes
|
|
24
21
|
* through policy to model skills; an alias never calls another alias. */
|
|
25
22
|
export const WORKIT_SKILL_ALIASES = {
|
|
26
|
-
"wk-
|
|
27
|
-
"wk-babysit": "workit-babysit",
|
|
23
|
+
"wk-shape": "workit-shape",
|
|
28
24
|
"wk-implement": "workit-implement",
|
|
29
|
-
"wk-plan": "workit-plan",
|
|
30
|
-
"wk-debug": "workit-debug",
|
|
31
25
|
"wk-review": "workit-review",
|
|
32
|
-
"wk-
|
|
33
|
-
"wk-
|
|
34
|
-
"wk-
|
|
35
|
-
"wk-deslop": "workit-deslop",
|
|
36
|
-
"wk-diagram": "workit-diagram",
|
|
37
|
-
"wk-mockup": "workit-mockup",
|
|
38
|
-
"wk-green-run": "workit-green-run",
|
|
39
|
-
"wk-steer": "workit-steer",
|
|
26
|
+
"wk-debug": "workit-debug",
|
|
27
|
+
"wk-ship": "workit-ship",
|
|
28
|
+
"wk-continue": "workit-continue",
|
|
40
29
|
"wk-bdd": "workit-bdd",
|
|
41
30
|
"wk-test-audit": "workit-test-audit",
|
|
42
|
-
|
|
31
|
+
"wk-deslop": "workit-deslop",
|
|
32
|
+
"wk-fanout": "workit-fanout",
|
|
33
|
+
"wk-verify-app": "workit-verify-app",
|
|
34
|
+
} as const satisfies Record<string, WorkitSkill>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The words a user types that should load each skill. Each skill's
|
|
38
|
+
* description and its bootstrap routing line carry every one of them, and no
|
|
39
|
+
* two skills share one (checked by test), so a trigger routes to one skill.
|
|
40
|
+
*/
|
|
41
|
+
export const WORKIT_SKILL_TRIGGERS: Readonly<Record<WorkitSkill, readonly string[]>> = {
|
|
42
|
+
"workit-shape": ["brainstorm", "plan", "spec", "grill", "should we"],
|
|
43
|
+
"workit-implement": ["implement", "build", "add a feature"],
|
|
44
|
+
"workit-review": ["review", "blast radius"],
|
|
45
|
+
"workit-debug": ["bug", "broken", "flaky", "regression"],
|
|
46
|
+
"workit-ship": ["ship", "babysit", "CI", "merge"],
|
|
47
|
+
"workit-continue": ["resume", "pick up", "handoff", "interruption"],
|
|
48
|
+
"workit-bdd": ["BDD", "TDD", "acceptance criteria", "Given/When/Then"],
|
|
49
|
+
"workit-test-audit": ["test audit", "tautology", "weak tests"],
|
|
50
|
+
"workit-deslop": ["deslop", "slop", "dead code"],
|
|
51
|
+
"workit-fanout": ["fan out", "parallelize", "parallel agents", "swarm"],
|
|
52
|
+
"workit-verify-app": ["verify the app", "smoke test", "prove it works"],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** The `description:` frontmatter value of a SKILL.md (one line, unquoted). */
|
|
56
|
+
export const skillDescription = (skillMd: string): string => {
|
|
57
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd)?.[1] ?? "";
|
|
58
|
+
const line = /^description:\s*(.+)$/m.exec(frontmatter)?.[1] ?? "";
|
|
59
|
+
return line.trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** A generated Cursor slash command for one alias; `commands/` is committed
|
|
63
|
+
* because Cursor discovers the plugin from git, so the build writes these and
|
|
64
|
+
* a drift test compares them to the committed copies. */
|
|
65
|
+
export const cursorCommandText = (alias: string, skill: string, description: string): string =>
|
|
66
|
+
`# /${alias}\n\nLoad and apply the bundled \`${skill}\` skill. ${description}\n\nExtra context: $ARGUMENTS\n`;
|
|
43
67
|
|
|
44
68
|
export const skillManifestNames = (root: string): string[] =>
|
|
45
69
|
existsSync(root)
|
package/src/core/store-lock.ts
CHANGED
|
@@ -4,9 +4,11 @@ import { hostname } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import * as z from "zod";
|
|
6
6
|
import { canonicalJson } from "./task-contract";
|
|
7
|
+
import { checkoutSlug, resolveStore } from "../store/paths";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* Ownership rules for a checkout's
|
|
10
|
+
* Ownership rules for the workit store's locks: a checkout's
|
|
11
|
+
* `checkouts/<slug>/metadata.lock` and each task's `tasks/<id>/lock`.
|
|
10
12
|
*
|
|
11
13
|
* The lock is a short mutex around one store mutation (or one managed effect).
|
|
12
14
|
* A lock whose owner is gone is reclaimed automatically; a lock whose owner is
|
|
@@ -197,7 +199,16 @@ const ageOf = (file: string, nowMs: number): number | null => {
|
|
|
197
199
|
}
|
|
198
200
|
};
|
|
199
201
|
|
|
200
|
-
|
|
202
|
+
/** A checkout's lock in the task store (see core/task-store.ts). */
|
|
203
|
+
export const lockPathFor = (root: string): string => {
|
|
204
|
+
let real = path.resolve(root);
|
|
205
|
+
try {
|
|
206
|
+
real = fs.realpathSync(real);
|
|
207
|
+
} catch {}
|
|
208
|
+
const location = resolveStore(real);
|
|
209
|
+
const dir = location instanceof Error ? path.join(real, ".workit") : location.dir;
|
|
210
|
+
return path.join(dir, "checkouts", checkoutSlug(real), "metadata.lock");
|
|
211
|
+
};
|
|
201
212
|
|
|
202
213
|
/** Remove a reclaim guard abandoned by a crashed reclaimer. Returns true when removed. */
|
|
203
214
|
export const clearAbandonedReclaimGuard = (lockPath: string, nowMs = Date.now()): boolean => {
|
|
@@ -1166,16 +1166,6 @@ const stateOperations = {
|
|
|
1166
1166
|
bundle: exportBundleSchema,
|
|
1167
1167
|
authorityRefs: z.array(refSchema),
|
|
1168
1168
|
}),
|
|
1169
|
-
recover: operation({
|
|
1170
|
-
action: z.literal("recover"),
|
|
1171
|
-
taskId: id.optional(),
|
|
1172
|
-
expectedWorkspaceRevision: revision.optional(),
|
|
1173
|
-
target: z.enum(["task", "workspace"]),
|
|
1174
|
-
expectedBytes: digest,
|
|
1175
|
-
snapshotDigest: digest,
|
|
1176
|
-
reason: text,
|
|
1177
|
-
authorityRefs: z.array(refSchema),
|
|
1178
|
-
}),
|
|
1179
1169
|
};
|
|
1180
1170
|
|
|
1181
1171
|
export const operationSchemas = {
|
|
@@ -1191,16 +1181,25 @@ export const operationSchemas = {
|
|
|
1191
1181
|
export type OperationRequest = z.infer<(typeof operationSchemas)[OperationFamily]>;
|
|
1192
1182
|
|
|
1193
1183
|
/**
|
|
1194
|
-
* Schemas advertised to hosts. `
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1197
|
-
* permission_denied. parseOperation still accepts it for embedders that do
|
|
1198
|
-
* supply that authority.
|
|
1184
|
+
* Schemas advertised to hosts. Every `taskId` is optional there: an operation
|
|
1185
|
+
* without one applies to the implicit task of the caller's branch (D3); the
|
|
1186
|
+
* engine fills it in before parsing against `operationSchemas`.
|
|
1199
1187
|
*/
|
|
1200
|
-
const
|
|
1188
|
+
const withImplicitTask = (options: Record<string, z.ZodObject>) =>
|
|
1189
|
+
Object.values(options).map((option) =>
|
|
1190
|
+
"taskId" in option.shape ? option.extend({ taskId: id.optional() }) : option,
|
|
1191
|
+
);
|
|
1192
|
+
const advertised = (options: Record<string, z.ZodObject>) =>
|
|
1193
|
+
z.discriminatedUnion("action", withImplicitTask(options) as any);
|
|
1201
1194
|
export const advertisedOperationSchemas = {
|
|
1202
|
-
|
|
1203
|
-
|
|
1195
|
+
task: advertised(taskOperations),
|
|
1196
|
+
policy: advertised(policyOperations),
|
|
1197
|
+
evidence: advertised(evidenceOperations),
|
|
1198
|
+
finding: advertised(findingOperations),
|
|
1199
|
+
decision: advertised(decisionOperations),
|
|
1200
|
+
worker: advertised(workerOperations),
|
|
1201
|
+
writer: advertised(writerOperations),
|
|
1202
|
+
state: advertised(stateOperations),
|
|
1204
1203
|
} as const;
|
|
1205
1204
|
export type TaskStartRequest = z.infer<typeof taskOperations.start>;
|
|
1206
1205
|
|
package/src/core/task-engine.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type Utc,
|
|
40
40
|
type Provenance,
|
|
41
41
|
type Worker,
|
|
42
|
+
type OperationFamily,
|
|
42
43
|
type WorkspaceRecord,
|
|
43
44
|
taskRecordSchema,
|
|
44
45
|
} from "./task-contract";
|
|
@@ -62,7 +63,8 @@ import {
|
|
|
62
63
|
} from "./authority";
|
|
63
64
|
import { diffPolicy, resolvePolicy } from "./policy-resolver";
|
|
64
65
|
import { verifyStandingApproval } from "./auto-approval";
|
|
65
|
-
import {
|
|
66
|
+
import { sameDirectoryIdentity, TaskStore } from "./task-store";
|
|
67
|
+
import { checkoutRootOf } from "../store/paths";
|
|
66
68
|
import { defaultLockTimeout } from "./store-lock";
|
|
67
69
|
import {
|
|
68
70
|
compactTaskContext,
|
|
@@ -108,12 +110,6 @@ export type OperationContext = {
|
|
|
108
110
|
now: Utc | (() => Utc);
|
|
109
111
|
nativeAuthority?: NativeAuthorityVerifier;
|
|
110
112
|
nativeWorker?: NativeWorkerVerifier;
|
|
111
|
-
nativeRecovery?: (input: {
|
|
112
|
-
lock: MetadataLock | null;
|
|
113
|
-
writer: WorkspaceRecord["writer"];
|
|
114
|
-
reason: string;
|
|
115
|
-
authorityRefs: Ref[];
|
|
116
|
-
}) => Result<ProcessEvidence>;
|
|
117
113
|
workerId?: string | null;
|
|
118
114
|
};
|
|
119
115
|
|
|
@@ -466,6 +462,40 @@ const refsWithinScope = (refs: Ref[], scope: Scope): boolean =>
|
|
|
466
462
|
bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] }),
|
|
467
463
|
);
|
|
468
464
|
|
|
465
|
+
/** Actions that take a taskId; without one they apply to the implicit task. */
|
|
466
|
+
const TASK_SCOPED_ACTIONS: ReadonlySet<string> = new Set([
|
|
467
|
+
"task.inspect",
|
|
468
|
+
"task.revise",
|
|
469
|
+
"task.progress",
|
|
470
|
+
"task.pause",
|
|
471
|
+
"task.resume",
|
|
472
|
+
"task.close",
|
|
473
|
+
"policy.assess",
|
|
474
|
+
"policy.preview",
|
|
475
|
+
"policy.explain",
|
|
476
|
+
"evidence.record",
|
|
477
|
+
"finding.record",
|
|
478
|
+
"finding.resolve",
|
|
479
|
+
"decision.record",
|
|
480
|
+
"decision.revoke",
|
|
481
|
+
"worker.assign",
|
|
482
|
+
"worker.report",
|
|
483
|
+
"worker.cancel",
|
|
484
|
+
"writer.acquire",
|
|
485
|
+
"writer.release",
|
|
486
|
+
"state.export",
|
|
487
|
+
]);
|
|
488
|
+
/** Recording actions that create the implicit task on first use. */
|
|
489
|
+
const CREATING_ACTIONS: ReadonlySet<string> = new Set([
|
|
490
|
+
"task.revise",
|
|
491
|
+
"task.progress",
|
|
492
|
+
"policy.assess",
|
|
493
|
+
"evidence.record",
|
|
494
|
+
"finding.record",
|
|
495
|
+
"decision.record",
|
|
496
|
+
"worker.assign",
|
|
497
|
+
"writer.acquire",
|
|
498
|
+
]);
|
|
469
499
|
/** Commit attempts for a call whose revisions the engine filled (see retryFilledRevisions). */
|
|
470
500
|
const REVISION_RETRY_ATTEMPTS = 8;
|
|
471
501
|
const retryPause = new Int32Array(new SharedArrayBuffer(4));
|
|
@@ -777,7 +807,10 @@ export class WorkitCore {
|
|
|
777
807
|
|
|
778
808
|
private contextRootError(): Result<null> {
|
|
779
809
|
try {
|
|
780
|
-
|
|
810
|
+
// The same checkout: any directory inside it resolves to its top level.
|
|
811
|
+
realpathSync(this.context.root);
|
|
812
|
+
const root = checkoutRootOf(this.context.root);
|
|
813
|
+
if (root !== this.store.root && !sameDirectoryIdentity(root, this.store.root))
|
|
781
814
|
return failure(
|
|
782
815
|
"invalid_input",
|
|
783
816
|
"operation context root does not match the task store root",
|
|
@@ -820,6 +853,36 @@ export class WorkitCore {
|
|
|
820
853
|
return retryFilledRevisions(filledRevisions(request), run);
|
|
821
854
|
}
|
|
822
855
|
|
|
856
|
+
/**
|
|
857
|
+
* A task-scoped request without a `taskId` applies to the implicit task of
|
|
858
|
+
* this checkout's branch (D3). Recording actions create that task on first
|
|
859
|
+
* use; reads and lifecycle actions only look it up. Helpers never create.
|
|
860
|
+
*/
|
|
861
|
+
private implicitTask(family: OperationFamily, request: unknown): Result<unknown> {
|
|
862
|
+
if (typeof request !== "object" || request === null || Array.isArray(request))
|
|
863
|
+
return success(null, null, request);
|
|
864
|
+
const value = request as { action?: unknown; taskId?: unknown };
|
|
865
|
+
if (value.taskId !== undefined || typeof value.action !== "string")
|
|
866
|
+
return success(null, null, request);
|
|
867
|
+
const action = `${family}.${value.action}`;
|
|
868
|
+
if (!TASK_SCOPED_ACTIONS.has(action)) return success(null, null, request);
|
|
869
|
+
const root = this.contextRootError();
|
|
870
|
+
if (!root.ok) return root;
|
|
871
|
+
const create = CREATING_ACTIONS.has(action) && (this.context.workerId ?? null) === null;
|
|
872
|
+
const found = this.store.implicitTask({
|
|
873
|
+
provenance: provenance(this.context),
|
|
874
|
+
create,
|
|
875
|
+
now: trustedNow(this.context),
|
|
876
|
+
});
|
|
877
|
+
if (!found.ok) return found;
|
|
878
|
+
if (!found.data)
|
|
879
|
+
return failure(
|
|
880
|
+
"not_found",
|
|
881
|
+
'no task for this branch yet; pass taskId, or record something first (`workit task start "<objective>"`, a note, a check) to create it',
|
|
882
|
+
);
|
|
883
|
+
return success(null, null, { ...request, taskId: found.data.task.id });
|
|
884
|
+
}
|
|
885
|
+
|
|
823
886
|
private helperEntry(
|
|
824
887
|
task: TaskRecord,
|
|
825
888
|
requireSession = false,
|
|
@@ -859,10 +922,12 @@ export class WorkitCore {
|
|
|
859
922
|
}
|
|
860
923
|
|
|
861
924
|
state(request: unknown): Result<ExportBundle | TaskSummary | TaskRecord | WorkspaceRecord> {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
925
|
+
const resolved = this.implicitTask("state", request);
|
|
926
|
+
if (!resolved.ok) return resolved;
|
|
927
|
+
const value = resolved.data;
|
|
928
|
+
return (value as { action?: unknown } | null)?.action === "import"
|
|
929
|
+
? this.retryOmittedRevisions(value, () => this.stateOnce(value))
|
|
930
|
+
: this.stateOnce(value);
|
|
866
931
|
}
|
|
867
932
|
|
|
868
933
|
private stateOnce(
|
|
@@ -932,47 +997,14 @@ export class WorkitCore {
|
|
|
932
997
|
if (!imported.ok) return imported;
|
|
933
998
|
return this.summary(imported.data);
|
|
934
999
|
}
|
|
935
|
-
|
|
936
|
-
return failure("permission_denied", "native recovery authority is unavailable");
|
|
937
|
-
const processEvidence = (
|
|
938
|
-
lock: MetadataLock | null,
|
|
939
|
-
writer: WorkspaceRecord["writer"],
|
|
940
|
-
): Result<ProcessEvidence> =>
|
|
941
|
-
this.context.nativeRecovery!({
|
|
942
|
-
lock,
|
|
943
|
-
writer,
|
|
944
|
-
reason: input.reason,
|
|
945
|
-
authorityRefs: input.authorityRefs,
|
|
946
|
-
});
|
|
947
|
-
if (input.target === "workspace")
|
|
948
|
-
return this.store.recoverWorkspace({
|
|
949
|
-
expectedBytes: input.expectedBytes,
|
|
950
|
-
snapshotDigest: input.snapshotDigest,
|
|
951
|
-
reason: input.reason,
|
|
952
|
-
authorityRefs: input.authorityRefs,
|
|
953
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
954
|
-
processEvidence,
|
|
955
|
-
});
|
|
956
|
-
const workspace = this.store.readWorkspace();
|
|
957
|
-
if (!workspace.ok) return workspace;
|
|
958
|
-
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
959
|
-
this.fillRevisions(input, undefined, workspace.data);
|
|
960
|
-
if (input.target === "workspace" && input.taskId !== undefined)
|
|
961
|
-
return failure("invalid_input", "workspace recovery takes no task ID");
|
|
962
|
-
if (input.target === "task" && input.taskId === undefined)
|
|
963
|
-
return failure("invalid_input", "task recovery requires a task ID");
|
|
964
|
-
return this.store.recoverTask(input.taskId, {
|
|
965
|
-
expectedBytes: input.expectedBytes,
|
|
966
|
-
snapshotDigest: input.snapshotDigest,
|
|
967
|
-
reason: input.reason,
|
|
968
|
-
authorityRefs: input.authorityRefs,
|
|
969
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
970
|
-
processEvidence,
|
|
971
|
-
});
|
|
1000
|
+
return failure("invalid_input", "unknown state action");
|
|
972
1001
|
}
|
|
973
1002
|
|
|
974
1003
|
task(request: unknown): Result<TaskSummary | TaskListItem[] | TaskView> {
|
|
975
|
-
|
|
1004
|
+
const resolved = this.implicitTask("task", request);
|
|
1005
|
+
if (!resolved.ok) return resolved;
|
|
1006
|
+
const value = resolved.data;
|
|
1007
|
+
return this.retryOmittedRevisions(value, () => this.taskOnce(value));
|
|
976
1008
|
}
|
|
977
1009
|
|
|
978
1010
|
private taskOnce(request: unknown): Result<TaskSummary | TaskListItem[] | TaskView> {
|
|
@@ -1083,7 +1115,10 @@ export class WorkitCore {
|
|
|
1083
1115
|
}
|
|
1084
1116
|
|
|
1085
1117
|
policy(request: unknown): Result<Policy | null> {
|
|
1086
|
-
|
|
1118
|
+
const resolved = this.implicitTask("policy", request);
|
|
1119
|
+
if (!resolved.ok) return resolved;
|
|
1120
|
+
const value = resolved.data;
|
|
1121
|
+
return this.retryOmittedRevisions(value, () => this.policyOnce(value));
|
|
1087
1122
|
}
|
|
1088
1123
|
|
|
1089
1124
|
private policyOnce(request: unknown): Result<Policy | null> {
|
|
@@ -1139,7 +1174,10 @@ export class WorkitCore {
|
|
|
1139
1174
|
}
|
|
1140
1175
|
|
|
1141
1176
|
evidence(request: unknown): Result<Entry<Evidence>> {
|
|
1142
|
-
|
|
1177
|
+
const resolved = this.implicitTask("evidence", request);
|
|
1178
|
+
if (!resolved.ok) return resolved;
|
|
1179
|
+
const value = resolved.data;
|
|
1180
|
+
return this.retryOmittedRevisions(value, () => this.evidenceOnce(value));
|
|
1143
1181
|
}
|
|
1144
1182
|
|
|
1145
1183
|
private evidenceOnce(request: unknown): Result<Entry<Evidence>> {
|
|
@@ -1339,11 +1377,13 @@ export class WorkitCore {
|
|
|
1339
1377
|
}
|
|
1340
1378
|
|
|
1341
1379
|
decision(request: unknown): Result<Entry<Decision>> {
|
|
1342
|
-
|
|
1380
|
+
const resolved = this.implicitTask("decision", request);
|
|
1381
|
+
return resolved.ok ? this.recordDecision(resolved.data) : resolved;
|
|
1343
1382
|
}
|
|
1344
1383
|
|
|
1345
1384
|
observeDecision(request: unknown, observation: unknown): Result<Entry<Decision>> {
|
|
1346
|
-
|
|
1385
|
+
const resolved = this.implicitTask("decision", request);
|
|
1386
|
+
return resolved.ok ? this.recordDecision(resolved.data, observation, true) : resolved;
|
|
1347
1387
|
}
|
|
1348
1388
|
|
|
1349
1389
|
/**
|
|
@@ -1353,7 +1393,8 @@ export class WorkitCore {
|
|
|
1353
1393
|
* removal, which fails the next reserve closed.
|
|
1354
1394
|
*/
|
|
1355
1395
|
observeStandingDecision(request: unknown): Result<Entry<Decision>> {
|
|
1356
|
-
|
|
1396
|
+
const resolved = this.implicitTask("decision", request);
|
|
1397
|
+
return resolved.ok ? this.recordDecision(resolved.data, undefined, true, true) : resolved;
|
|
1357
1398
|
}
|
|
1358
1399
|
|
|
1359
1400
|
/**
|
|
@@ -1576,7 +1617,10 @@ export class WorkitCore {
|
|
|
1576
1617
|
}
|
|
1577
1618
|
|
|
1578
1619
|
finding(request: unknown): Result<Entry<Finding>> {
|
|
1579
|
-
|
|
1620
|
+
const resolved = this.implicitTask("finding", request);
|
|
1621
|
+
if (!resolved.ok) return resolved;
|
|
1622
|
+
const value = resolved.data;
|
|
1623
|
+
return this.retryOmittedRevisions(value, () => this.findingOnce(value));
|
|
1580
1624
|
}
|
|
1581
1625
|
|
|
1582
1626
|
private findingOnce(request: unknown): Result<Entry<Finding>> {
|
|
@@ -1749,7 +1793,10 @@ export class WorkitCore {
|
|
|
1749
1793
|
}
|
|
1750
1794
|
|
|
1751
1795
|
worker(request: unknown): Result<Entry<Worker>> {
|
|
1752
|
-
|
|
1796
|
+
const resolved = this.implicitTask("worker", request);
|
|
1797
|
+
if (!resolved.ok) return resolved;
|
|
1798
|
+
const value = resolved.data;
|
|
1799
|
+
return this.retryOmittedRevisions(value, () => this.workerOnce(value));
|
|
1753
1800
|
}
|
|
1754
1801
|
|
|
1755
1802
|
private workerOnce(request: unknown): Result<Entry<Worker>> {
|
|
@@ -2188,7 +2235,10 @@ export class WorkitCore {
|
|
|
2188
2235
|
}
|
|
2189
2236
|
|
|
2190
2237
|
writer(request: unknown): Result<WorkspaceRecord> {
|
|
2191
|
-
|
|
2238
|
+
const resolved = this.implicitTask("writer", request);
|
|
2239
|
+
if (!resolved.ok) return resolved;
|
|
2240
|
+
const value = resolved.data;
|
|
2241
|
+
return this.retryOmittedRevisions(value, () => this.writerOnce(value));
|
|
2192
2242
|
}
|
|
2193
2243
|
|
|
2194
2244
|
private writerOnce(request: unknown): Result<WorkspaceRecord> {
|