@workit-dev/claude-code 0.0.0-stage → 1.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/.claude-plugin/plugin.json +19 -0
- package/README.md +48 -2
- package/agents/implementer.md +56 -0
- package/agents/reviewer.md +33 -0
- package/agents/verifier.md +40 -0
- package/assets/templates/headers.md +3 -0
- package/assets/templates/hygiene/.editorconfig +8 -0
- package/assets/templates/hygiene/.gitattributes +3 -0
- package/assets/templates/hygiene/CHANGELOG.md +14 -0
- package/assets/templates/hygiene/CONTRIBUTING.md +3 -0
- package/assets/templates/hygiene/LICENSE +21 -0
- package/assets/templates/hygiene/README.md +3 -0
- package/assets/templates/issue-update.md +5 -0
- package/assets/templates/plan-template.md +26 -0
- package/assets/templates/spec-template.md +64 -0
- package/assets/templates/workit-contract.md +13 -0
- package/bin/node-resolve.mjs +249 -0
- package/bin/workit +82 -0
- package/bin/workit-hook.mjs +552 -0
- package/bin/workit-paths.mjs +242 -0
- package/dist/workit-hook.js +248 -0
- package/dist/workit.js +122606 -0
- package/hooks/hooks.json +147 -0
- package/package.json +47 -4
- package/skills/architecture/SKILL.md +72 -0
- package/skills/architecture/agents/openai.yaml +3 -0
- package/skills/architecture/references/instruction-files.md +52 -0
- package/skills/architecture/references/vocabulary.md +53 -0
- package/skills/bdd/SKILL.md +53 -0
- package/skills/continue/SKILL.md +54 -0
- package/skills/debug/SKILL.md +45 -0
- package/skills/deslop/SKILL.md +58 -0
- package/skills/fanout/SKILL.md +71 -0
- package/skills/fanout/references/brief.md +141 -0
- package/skills/fanout/references/orchestration.md +169 -0
- package/skills/implement/SKILL.md +71 -0
- package/skills/retro/SKILL.md +72 -0
- package/skills/retro/agents/openai.yaml +3 -0
- package/skills/retro/references/sources.md +35 -0
- package/skills/retro/references/steering.md +39 -0
- package/skills/review/SKILL.md +66 -0
- package/skills/review/references/impact.md +24 -0
- package/skills/shape/SKILL.md +71 -0
- package/skills/shape/references/diagrams.md +17 -0
- package/skills/shape/references/knowledge.md +61 -0
- package/skills/shape/references/mockups.md +15 -0
- package/skills/shape/references/slicing.md +44 -0
- package/skills/ship/SKILL.md +71 -0
- package/skills/ship/references/changelog.md +44 -0
- package/skills/ship/references/pr-body.md +32 -0
- package/skills/ship/references/release.md +39 -0
- package/skills/test-audit/SKILL.md +54 -0
- package/skills/verify-app/SKILL.md +68 -0
- package/skills/verify-app/references/template.md +51 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "workit",
|
|
3
|
+
"displayName": "Workit",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Workflow rails for agentic coding: task context on every session, branch policy on shell commands, and the workit method skills",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "BrainerVirus"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/BrainerVirus/workit#readme",
|
|
10
|
+
"repository": "https://github.com/BrainerVirus/workit",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"agentic-coding",
|
|
14
|
+
"branch-policy",
|
|
15
|
+
"skills",
|
|
16
|
+
"task-management",
|
|
17
|
+
"workflow"
|
|
18
|
+
]
|
|
19
|
+
}
|
package/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @workit-dev/claude-code
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/BrainerVirus/workit/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/BrainerVirus/workit/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
[Workit](https://github.com/BrainerVirus/workit) plugin for Claude Code: task context on session start and per turn,
|
|
7
|
+
branch policy on `git` shell commands, the before-write gate on edits and shell
|
|
8
|
+
writes, thirteen method skills (`/workit:<name>`),
|
|
9
|
+
and `verifier`, `reviewer` and `implementer` agents.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Latest published (the repository root is the marketplace; its entry installs
|
|
14
|
+
this npm package):
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
claude plugin marketplace add BrainerVirus/workit
|
|
18
|
+
claude plugin install workit@workit
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Update with `workit upgrade` (native fallback: `claude plugin marketplace update workit && claude plugin update workit@workit`), then restart Claude Code; uninstall with `workit uninstall`.
|
|
22
|
+
|
|
23
|
+
Local pin to a checkout (hooks and `bin/workit` run the sources with Bun):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bun install
|
|
27
|
+
bun packages/workit-claude-code/scripts/build.ts --skills-only
|
|
28
|
+
claude --plugin-dir "$PWD/packages/workit-claude-code"
|
|
29
|
+
# or, for every session:
|
|
30
|
+
export CLAUDE_CODE_PLUGIN_DIRS="$HOME/path/to/workit/packages/workit-claude-code"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`WORKIT_CLAUDE_RUNTIME=source|dist` forces one runtime;
|
|
34
|
+
`WORKIT_SHIM_TRACE=1 workit …` prints which `workit` entry ran.
|
|
35
|
+
|
|
36
|
+
## Layout
|
|
37
|
+
|
|
38
|
+
| Path | Purpose |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `.claude-plugin/plugin.json` | Manifest (version synced at release) |
|
|
41
|
+
| `hooks/hooks.json` | SessionStart (startup/resume/clear/compact/fork), UserPromptSubmit, PreToolUse (Bash, PowerShell, Edit/Write/MultiEdit/NotebookEdit), SubagentStart |
|
|
42
|
+
| `bin/workit-hook.mjs` | Hook launcher: source with Bun in a checkout, `dist/workit-hook.js` when installed; answers plain shell commands and edits in task-less checkouts with `{}` before loading the runtime; fails open (`{}` + one stderr line) when neither can run |
|
|
43
|
+
| `bin/workit` | `workit` on the Bash tool's PATH (same source/dist switch) |
|
|
44
|
+
| `agents/` | `verifier`, `reviewer` (read-only), `implementer` (works in the lead's `workit fanout worktree create` worktree; no `isolation`) |
|
|
45
|
+
| `skills/` | Generated by `scripts/build.ts` from `packages/workit-core/skills` (not committed) |
|
|
46
|
+
|
|
47
|
+
Requires Node.js 24+ (installed plugin) or Bun (local pin).
|
|
48
|
+
|
|
49
|
+
Full guide: [Claude Code](https://github.com/BrainerVirus/workit/blob/main/docs/guides/claude-code.md).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: implementer
|
|
3
|
+
description: Implements one scoped workit brief in the git worktree its lead made with `workit fanout worktree create`, and reports what changed and how it was checked. Use to delegate a well-defined slice (goal, scope, acceptance, verify, forbidden, report, worktree) so it can run in parallel with other work.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are a workit implementer. You work only in the git worktree your brief
|
|
7
|
+
names (its WORKTREE line), which the lead made with
|
|
8
|
+
`workit fanout worktree create <slice>` before spawning you; never in the
|
|
9
|
+
lead's checkout.
|
|
10
|
+
|
|
11
|
+
Refuse to start without a complete brief. It must state: goal, scope (files
|
|
12
|
+
or areas you may touch), acceptance (observable outcomes), verify (exact
|
|
13
|
+
commands), timebox, forbidden (what you must not touch or do), report (what to
|
|
14
|
+
hand back), standing orders, mode (`new` or `resume`) and the worktree path.
|
|
15
|
+
If a field is missing, report which and stop.
|
|
16
|
+
|
|
17
|
+
1. In a Workit worktree (the default), run each git and check command against
|
|
18
|
+
it, one verb per call:
|
|
19
|
+
`workit git --cwd <worktree> commit …`, `workit check --cwd <worktree> <name>`
|
|
20
|
+
(verb first, so the host's allow rules match).
|
|
21
|
+
Never `cd <worktree> && …` chains, `$VAR` paths or compound commands. Edit
|
|
22
|
+
files by their path under the worktree. Workit's hooks deny an edit or a
|
|
23
|
+
mutating git command outside it.
|
|
24
|
+
2. The worktree is already on the brief's branch. If it is not: `MODE: new`,
|
|
25
|
+
`workit git --cwd <worktree> branch <branch> --base <base>`; `MODE: resume`
|
|
26
|
+
(you replace a stopped worker), `git -C <worktree> switch <branch>` and
|
|
27
|
+
continue from its head. A hook denies protected or non-compliant names;
|
|
28
|
+
follow the unblock it prints.
|
|
29
|
+
3. Stay inside the scope. Anything outside it is a follow-up in the report,
|
|
30
|
+
not a diff. Follow the workit-implement skill (`/workit:implement`).
|
|
31
|
+
4. Decide ambiguities yourself and record them:
|
|
32
|
+
`workit ledger ruling "<what>" --why "<why>" --cost-if-wrong "<cost>"`.
|
|
33
|
+
Stop only for an irreversible or security-sensitive action, or a side effect
|
|
34
|
+
outside your worktree.
|
|
35
|
+
5. Commit in small steps with
|
|
36
|
+
`workit git --cwd <worktree> commit -m "<conventional message>" -- <paths in scope>`.
|
|
37
|
+
Add Co-Authored-By in the same commit with
|
|
38
|
+
`workit git commit --trailer "Co-Authored-By: <name> <email>" -- <paths>`
|
|
39
|
+
(the brief's RULES give your host's exact line), never by amending.
|
|
40
|
+
6. Run every verify command as `workit check --cwd <worktree> …` and keep the
|
|
41
|
+
real exit codes.
|
|
42
|
+
7. Never record a verdict on your own work and never pass `--self`: a separate
|
|
43
|
+
verifier judges it. Never push, open a PR, rebase or merge unless the brief
|
|
44
|
+
says so.
|
|
45
|
+
|
|
46
|
+
In a Claude Code host-isolated worktree (`.claude/worktrees/agent-<id>`,
|
|
47
|
+
whose SubagentStart text says "Run git as plain") the rule flips: run git as
|
|
48
|
+
plain commands there, never `workit git …` (Claude Code's isolation refuses a
|
|
49
|
+
git wrapper), and keep workit for `check`, `ledger` and `pr`.
|
|
50
|
+
|
|
51
|
+
Before your final report, wait for or stop every background command you started
|
|
52
|
+
(the host shows you running while one lives); never report with one still running.
|
|
53
|
+
|
|
54
|
+
Report: worktree, branch and head SHA, files changed, each verify command with
|
|
55
|
+
its exit code, each acceptance item met or not, rulings recorded, and any
|
|
56
|
+
deviation from the brief.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
description: Fresh-context reviewer for a workit branch or PR - spec fidelity and standards as separate axes, test quality and blast radius - that records findings and a non-author verdict. Use for independent review of a diff, branch or PR.
|
|
4
|
+
tools: Read, Grep, Glob, Bash, Skill
|
|
5
|
+
disallowedTools: Write, Edit, NotebookEdit
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are the workit reviewer. Review the real candidate, never the author's
|
|
9
|
+
description of it. Follow the workit-review skill (`/workit:review`).
|
|
10
|
+
|
|
11
|
+
1. Pin the candidate: `git rev-parse HEAD`, the base, `git diff <base>...HEAD`;
|
|
12
|
+
for a PR, `workit pr status --json` for checks and unresolved threads.
|
|
13
|
+
2. Read the intent: brief or acceptance criteria, spec, PR body, and
|
|
14
|
+
`workit ledger list --type decision`.
|
|
15
|
+
3. Judge two axes separately: spec fidelity (missing, creep, wrong; quote the
|
|
16
|
+
line) and standards (repo rules first, then smells). Then test quality
|
|
17
|
+
(`workit test-audit --diff`) and blast radius: one fact plus one proof per
|
|
18
|
+
touched contract or caller, run with `workit check -- <cmd>`; label anything
|
|
19
|
+
unproven UNPROVEN.
|
|
20
|
+
4. Each finding: file:line, severity (blocker, major, minor, nit), evidence
|
|
21
|
+
(hunk, test or command output), concrete fix.
|
|
22
|
+
5. Record the verdict:
|
|
23
|
+
start from the exact verdict command the SubagentStart hook put in your context (it
|
|
24
|
+
carries your session and `--token=<token>`; the token is yours alone: never put it in a report); it already names `--kind review`:
|
|
25
|
+
`workit ledger verdict verified|failed|blocked --kind review --branch <b> --session <id> --token=<token> --how "<what you read and ran>"`
|
|
26
|
+
(`verified` with `--kind review`: no blocker or major finding left).
|
|
27
|
+
Record a design choice you had to make as `workit ledger ruling`.
|
|
28
|
+
|
|
29
|
+
Before your final report, wait for or stop every background command you started
|
|
30
|
+
(the host shows you running while one lives); never report with one still running.
|
|
31
|
+
|
|
32
|
+
Stay read-only: read and test commands only, never commits, pushes, merges or
|
|
33
|
+
edits. If the ledger refuses you as an author, report that instead of a verdict.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verifier
|
|
3
|
+
description: Independently verifies a workit candidate (branch, commit or PR) on its real surface and records a SHA-keyed verdict in the ledger. Use after an implementer finishes and before shipping, never on work this session authored.
|
|
4
|
+
tools: Read, Grep, Glob, Bash, Skill
|
|
5
|
+
disallowedTools: Write, Edit, NotebookEdit
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are the workit verifier. You did not write the candidate and you never
|
|
9
|
+
change it: you observe, judge and record. Start from the goal and work
|
|
10
|
+
backward: what must be true, what must exist, what must be wired. The author's
|
|
11
|
+
summary is a claim, not evidence; assume the goal is not met until you observe
|
|
12
|
+
it.
|
|
13
|
+
|
|
14
|
+
1. Pin the candidate in its own checkout (run workit as
|
|
15
|
+
`workit <verb> --cwd <worktree> …`, verb first; never `cd` first): `git -C <worktree> rev-parse HEAD` and `git -C <worktree> status --short`. A dirty tree
|
|
16
|
+
or a moving HEAD is itself a finding, and the ledger refuses a verdict on it.
|
|
17
|
+
2. Read the brief: goal, acceptance (Given/When/Then), verify commands.
|
|
18
|
+
3. Run every verify command through the CLI so the result is observed:
|
|
19
|
+
`workit check <name>` for configured checks, `workit check -- <cmd>` for the
|
|
20
|
+
rest. Never substitute an easier command. Run every configured check by name
|
|
21
|
+
(your SubagentStart context lists them) or cite green CI on that head; otherwise, `verified` is refused.
|
|
22
|
+
4. Drive the real surface with the project's `verify-<app>` skill when it
|
|
23
|
+
exists (`/workit:verify-app` generates one). Tests alone are unit evidence.
|
|
24
|
+
5. Judge each acceptance line as met, not met, or not verifiable, citing what
|
|
25
|
+
you observed. Label every claim measured, inferred or guess.
|
|
26
|
+
6. Record the verdict, then report it:
|
|
27
|
+
start from the exact verdict command the SubagentStart hook put in your context (it
|
|
28
|
+
carries your session and `--token=<token>`; you share the lead's
|
|
29
|
+
`WORKIT_SESSION_ID`, which authored the work) and pick its `--kind <live|unit>`:
|
|
30
|
+
`workit ledger verdict verified|tests-verified|type-check-only|failed|blocked --kind <live|unit> --branch <b> --session <id> --token=<token> --how "<what you ran and saw>"`.
|
|
31
|
+
`verified` means the change was observed working on its real surface
|
|
32
|
+
(verify-<app>, `--kind live`); `tests-verified` means only tests ran. If the ledger refuses you as an author, report that;
|
|
33
|
+
never pass `--self` to get around it.
|
|
34
|
+
|
|
35
|
+
Before your final report, wait for or stop every background command you started
|
|
36
|
+
(the host shows you running while one lives); never report with one still running.
|
|
37
|
+
|
|
38
|
+
Report: candidate SHA, each check with its exit code, each acceptance line,
|
|
39
|
+
the verdict row you recorded. Do not fix anything you find: a failing check is
|
|
40
|
+
a verdict, not a task.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) <YEAR> <HOLDER>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# <Feature> plan
|
|
2
|
+
|
|
3
|
+
Spec: `docs/<slug>/spec.md` · Goal: <one sentence>
|
|
4
|
+
|
|
5
|
+
A plan records decisions, not code: per slice the branch, what it touches, its
|
|
6
|
+
acceptance, how it is verified, and what it depends on (workit-shape
|
|
7
|
+
`references/slicing.md`). Independent slices fan out (workit-fanout);
|
|
8
|
+
dependent ones stack (`workit stack plan <bottom> … <top>`).
|
|
9
|
+
|
|
10
|
+
## Global constraints
|
|
11
|
+
|
|
12
|
+
- <project-wide requirement, one line each>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
### S1 <type>/<slug> (base: main)
|
|
17
|
+
|
|
18
|
+
Touches: <areas, not line numbers>
|
|
19
|
+
Acceptance:
|
|
20
|
+
- Given <state>, When <action>, Then <observable result>
|
|
21
|
+
Verify: `workit check test`; verify-<app> "<feature>"
|
|
22
|
+
Decisions: <ledger refs or one-line rulings>
|
|
23
|
+
|
|
24
|
+
| Slice | Branch | Depends on | Status |
|
|
25
|
+
| --- | --- | --- | --- |
|
|
26
|
+
| S1 | <type>/<slug> | — | pending |
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Spec: <feature>
|
|
2
|
+
|
|
3
|
+
**Branch:** `feature/<slug>`
|
|
4
|
+
|
|
5
|
+
**Change:** ADDED|MODIFIED|REMOVED — <one-sentence intent>. (Small work with
|
|
6
|
+
no durable agreement: `Spec: none (<reason>)`, no file.)
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
<!-- Why does this exist? What problem does it solve? 1-3 sentences. -->
|
|
11
|
+
|
|
12
|
+
## Goals
|
|
13
|
+
|
|
14
|
+
- <!-- measurable, one per bullet -->
|
|
15
|
+
|
|
16
|
+
## Non-goals
|
|
17
|
+
|
|
18
|
+
- <!-- explicitly out of scope -->
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
|
|
22
|
+
<!-- Optional: only when a diagram argues a decision (workit-shape
|
|
23
|
+
references/diagrams.md); otherwise delete this section. -->
|
|
24
|
+
```mermaid
|
|
25
|
+
flowchart TD
|
|
26
|
+
A[Start] --> B[Step]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
<!-- Optional: a UI choice still open gets an ASCII sketch (workit-shape
|
|
30
|
+
references/mockups.md). -->
|
|
31
|
+
```text
|
|
32
|
+
┌──────────────┐
|
|
33
|
+
│ Header │
|
|
34
|
+
└──────────────┘
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Data flow / contracts
|
|
38
|
+
|
|
39
|
+
<!-- REQUIRED when there is a glossary, scope comparison, or contracts: use markdown tables. -->
|
|
40
|
+
| Term | Meaning |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| <term> | <meaning> |
|
|
43
|
+
|
|
44
|
+
## Acceptance criteria
|
|
45
|
+
|
|
46
|
+
<!-- REQUIRED: enumerable, each verifiable. Numbered CA-01, CA-02, ... -->
|
|
47
|
+
<!-- Requirements use SHALL/MUST (one per bullet, observable, no HOW); each
|
|
48
|
+
carries at least one GIVEN/WHEN/THEN, including the most-regretted edge. -->
|
|
49
|
+
- CA-01 …
|
|
50
|
+
|
|
51
|
+
## Review checklist
|
|
52
|
+
|
|
53
|
+
<!-- Before implementation: intent matches; nothing extra; each requirement
|
|
54
|
+
testable with an exercising scenario; most-cared case covered;
|
|
55
|
+
tasks trace to requirements; you would sign if built exactly as written. -->
|
|
56
|
+
- [ ] …
|
|
57
|
+
|
|
58
|
+
## Decisions
|
|
59
|
+
|
|
60
|
+
- D-01 …
|
|
61
|
+
|
|
62
|
+
## Future work
|
|
63
|
+
|
|
64
|
+
- …
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Workit contract
|
|
2
|
+
|
|
3
|
+
Workit keeps one accountable lead and one shared task state. Inspect current
|
|
4
|
+
task state before acting; use only the shared operations for task, policy,
|
|
5
|
+
evidence, finding, decision, worker, and state changes. Authority is
|
|
6
|
+
bounded by the requested scope, current revision, caller/session provenance, and
|
|
7
|
+
observed capabilities; delivery is bounded by the workspace autonomy grants
|
|
8
|
+
(`workit grant show`). Never claim host enforcement or evidence that the host
|
|
9
|
+
cannot provide. Preserve unresolved requirements, gaps, and uncertain workers.
|
|
10
|
+
|
|
11
|
+
Focused methods are loaded only when the resolved policy requires them. A method
|
|
12
|
+
must call the shared operations and must not create a second lifecycle, approval
|
|
13
|
+
chain, or task-state representation.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Finds a Node that can run a Workit hook when the PATH node is below the
|
|
2
|
+
// package's engines floor (a repo pinned to Node 16/18/22 by a version
|
|
3
|
+
// manager). Plain JavaScript with node: builtins only and no syntax newer than
|
|
4
|
+
// Node 16 supports: the host launchers import it (or carry a byte-equal copy,
|
|
5
|
+
// a test holds them equal) and must start on the very Node they are replacing.
|
|
6
|
+
// Side-effect free: importing it runs nothing; no process is spawned here.
|
|
7
|
+
//
|
|
8
|
+
// Order: (1) $WORKIT_NODE; (2) the node `workit setup`/`upgrade` recorded in
|
|
9
|
+
// <state dir>/node.json (workit-paths.mjs); (3) a scan of version-manager installs (fnm, nvm,
|
|
10
|
+
// volta, mise, asdf and the Windows layouts), highest version >= the floor.
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { stateDir } from "./workit-paths.mjs";
|
|
16
|
+
|
|
17
|
+
/** The Node major the hooks need; a test holds it equal to every package's engines.node. */
|
|
18
|
+
export const HOOK_NODE_FLOOR = [24, 0, 0];
|
|
19
|
+
|
|
20
|
+
/** Set on the re-executed child so a launcher never re-executes twice. */
|
|
21
|
+
export const HOOK_NODE_REEXEC_ENV = "WORKIT_HOOK_REEXEC";
|
|
22
|
+
|
|
23
|
+
const MAX_ENTRIES_PER_ROOT = 200;
|
|
24
|
+
|
|
25
|
+
/** @param {unknown} text @returns {number[] | null} */
|
|
26
|
+
export const parseNodeVersion = (text) => {
|
|
27
|
+
const found = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(String(text).trim());
|
|
28
|
+
return found ? [Number(found[1]), Number(found[2] || 0), Number(found[3] || 0)] : null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** @param {number[]} a @param {number[]} b negative when a < b */
|
|
32
|
+
export const compareVersions = (a, b) => {
|
|
33
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] - b[i];
|
|
34
|
+
return 0;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The `>=X[.Y[.Z]]` minimum of an engines.node range, or null. @param {unknown} range */
|
|
38
|
+
export const floorOfRange = (range) => {
|
|
39
|
+
const min = /^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(String(range === undefined ? "" : range));
|
|
40
|
+
return min ? [Number(min[1]), Number(min[2] || 0), Number(min[3] || 0)] : null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Workit's state dir (workit-paths.mjs, the one resolver). @param {NodeJS.ProcessEnv} env */
|
|
44
|
+
export const hookStateDir = (env) => stateDir(env);
|
|
45
|
+
|
|
46
|
+
/** @param {string} file */
|
|
47
|
+
const isFile = (file) => {
|
|
48
|
+
try {
|
|
49
|
+
return statSync(file).isFile();
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** @param {string} file */
|
|
56
|
+
const readJson = (file) => {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The version-manager directories that hold one node per child directory:
|
|
66
|
+
* `{ dir, bin }` where the node is `<dir>/<child>/<bin>`.
|
|
67
|
+
* @param {NodeJS.ProcessEnv} env @param {string} platform
|
|
68
|
+
*/
|
|
69
|
+
const scanRoots = (env, platform) => {
|
|
70
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
71
|
+
const win = platform === "win32";
|
|
72
|
+
const exe = win ? "node.exe" : "node";
|
|
73
|
+
const bin = win ? exe : path.join("bin", exe);
|
|
74
|
+
/** @type {Array<{ dir: string, bin: string }>} */
|
|
75
|
+
const roots = [];
|
|
76
|
+
const add = (dir, rel) => {
|
|
77
|
+
if (dir) roots.push({ dir, bin: rel });
|
|
78
|
+
};
|
|
79
|
+
// fnm: <dir>/node-versions/<v>/installation/bin/node
|
|
80
|
+
const fnmDirs = [
|
|
81
|
+
env.FNM_DIR,
|
|
82
|
+
win ? path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "fnm") : "",
|
|
83
|
+
win ? path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "fnm") : "",
|
|
84
|
+
env.XDG_DATA_HOME ? path.join(env.XDG_DATA_HOME, "fnm") : "",
|
|
85
|
+
path.join(home, ".local", "share", "fnm"),
|
|
86
|
+
path.join(home, "Library", "Application Support", "fnm"),
|
|
87
|
+
path.join(home, ".fnm"),
|
|
88
|
+
];
|
|
89
|
+
for (const dir of fnmDirs)
|
|
90
|
+
if (dir) add(path.join(dir, "node-versions"), path.join("installation", bin));
|
|
91
|
+
// nvm: <dir>/versions/node/<v>/bin/node; nvm-windows: <NVM_HOME>/<v>/node.exe
|
|
92
|
+
for (const dir of [env.NVM_DIR, path.join(home, ".nvm")])
|
|
93
|
+
if (dir) add(path.join(dir, "versions", "node"), bin);
|
|
94
|
+
if (env.NVM_HOME) add(env.NVM_HOME, exe);
|
|
95
|
+
// volta: <dir>/tools/image/node/<v>/bin/node
|
|
96
|
+
const volta = env.VOLTA_HOME || (win ? "" : path.join(home, ".volta"));
|
|
97
|
+
if (volta) add(path.join(volta, "tools", "image", "node"), bin);
|
|
98
|
+
if (win)
|
|
99
|
+
add(
|
|
100
|
+
path.join(
|
|
101
|
+
env.LOCALAPPDATA || path.join(home, "AppData", "Local"),
|
|
102
|
+
"Volta",
|
|
103
|
+
"tools",
|
|
104
|
+
"image",
|
|
105
|
+
"node",
|
|
106
|
+
),
|
|
107
|
+
bin,
|
|
108
|
+
);
|
|
109
|
+
// mise and asdf: <dir>/installs/node[js]/<v>/bin/node
|
|
110
|
+
const mise = env.MISE_DATA_DIR || path.join(home, ".local", "share", "mise");
|
|
111
|
+
add(path.join(mise, "installs", "node"), bin);
|
|
112
|
+
const asdf = env.ASDF_DATA_DIR || path.join(home, ".asdf");
|
|
113
|
+
add(path.join(asdf, "installs", "nodejs"), bin);
|
|
114
|
+
return roots;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Every installed node a version manager lists, with its version: directory
|
|
119
|
+
* listings only, at most MAX_ENTRIES_PER_ROOT per root.
|
|
120
|
+
* @param {NodeJS.ProcessEnv} env @param {string} [platform]
|
|
121
|
+
* @returns {Array<{ path: string, version: number[], source: string }>}
|
|
122
|
+
*/
|
|
123
|
+
export const scanInstalledNodes = (env, platform = process.platform) => {
|
|
124
|
+
/** @type {Array<{ path: string, version: number[], source: string }>} */
|
|
125
|
+
const found = [];
|
|
126
|
+
for (const root of scanRoots(env, platform)) {
|
|
127
|
+
let names;
|
|
128
|
+
try {
|
|
129
|
+
names = readdirSync(root.dir).slice(0, MAX_ENTRIES_PER_ROOT);
|
|
130
|
+
} catch {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const name of names) {
|
|
134
|
+
const version = parseNodeVersion(name);
|
|
135
|
+
if (!version || !/^v?\d/.test(name)) continue;
|
|
136
|
+
// volta's layout nests nothing; fnm's, nvm's, mise's and asdf's differ only in `bin`.
|
|
137
|
+
const candidate = path.join(root.dir, name, root.bin);
|
|
138
|
+
if (isFile(candidate)) found.push({ path: candidate, version, source: root.dir });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return found;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The node the hooks should run on, or null. `floor` is [major, minor, patch].
|
|
146
|
+
* @param {{ env: NodeJS.ProcessEnv, floor: number[], platform?: string }} options
|
|
147
|
+
* @returns {{ path: string, version: string | null, source: "env" | "recorded" | "scan" } | null}
|
|
148
|
+
*/
|
|
149
|
+
export const resolveHookNode = ({ env, floor, platform = process.platform }) => {
|
|
150
|
+
if (env.WORKIT_NODE && isFile(env.WORKIT_NODE))
|
|
151
|
+
return { path: env.WORKIT_NODE, version: null, source: "env" };
|
|
152
|
+
const recorded = readJson(path.join(hookStateDir(env), "node.json"));
|
|
153
|
+
if (recorded && typeof recorded.path === "string" && isFile(recorded.path)) {
|
|
154
|
+
const version = parseNodeVersion(recorded.version);
|
|
155
|
+
if (version && compareVersions(version, floor) >= 0)
|
|
156
|
+
return { path: recorded.path, version: String(recorded.version), source: "recorded" };
|
|
157
|
+
}
|
|
158
|
+
let best = null;
|
|
159
|
+
for (const entry of scanInstalledNodes(env, platform))
|
|
160
|
+
if (
|
|
161
|
+
compareVersions(entry.version, floor) >= 0 &&
|
|
162
|
+
(!best || compareVersions(entry.version, best.version) > 0)
|
|
163
|
+
)
|
|
164
|
+
best = entry;
|
|
165
|
+
return best ? { path: best.path, version: best.version.join("."), source: "scan" } : null;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The fail-open line for a node below the floor: null when `found` is at or
|
|
170
|
+
* above `floor`, or unparseable (an unreadable version never blocks).
|
|
171
|
+
* @param {string} found @param {number[] | null} floor @param {string} range
|
|
172
|
+
*/
|
|
173
|
+
export const floorMessage = (found, floor, range) => {
|
|
174
|
+
if (!floor) return null;
|
|
175
|
+
const have = parseNodeVersion(found);
|
|
176
|
+
if (!have || compareVersions(have, floor) >= 0) return null;
|
|
177
|
+
return `needs Node ${range}, found ${found}`;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** What the agent is told once, at SessionStart, when no Node >= the floor resolves. */
|
|
181
|
+
export const nodeFloorNotice = (found, range) =>
|
|
182
|
+
`Workit hooks are off: the Node on PATH is ${String(found).replace(/^v/, "")}, below the required ${range}, and no Node 24+ was found. ` +
|
|
183
|
+
"Install Node 24 (fnm install 24, nvm install 24) or run `workit setup` from a Node 24 shell so Workit can record it; " +
|
|
184
|
+
"`workit doctor` (check hook_node) shows what the hooks run on.";
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Where setup and upgrade record the Node they run on. A no-op for a non-node
|
|
188
|
+
* binary (bun) or a Node below the floor.
|
|
189
|
+
* @param {{ env: NodeJS.ProcessEnv, execPath: string, version: string, floor: number[] }} options
|
|
190
|
+
* @returns {string | null} the file written
|
|
191
|
+
*/
|
|
192
|
+
export const nodeRecord = ({ env, execPath, version, floor }) => {
|
|
193
|
+
const parsed = parseNodeVersion(version);
|
|
194
|
+
if (!parsed || compareVersions(parsed, floor) < 0) return null;
|
|
195
|
+
if (!/^node(?:\.exe)?$/i.test(path.basename(execPath))) return null;
|
|
196
|
+
return path.join(hookStateDir(env), "node.json");
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Run `script args…` on `nodePath` with this process's stdin, stdout and
|
|
201
|
+
* stderr passed through. The child carries the re-exec guard and a real
|
|
202
|
+
* version (WORKIT_HOOK_NODE_VERSION, a test seam, is dropped).
|
|
203
|
+
* @returns {number | null} the child's exit code, or null when it could not start
|
|
204
|
+
*/
|
|
205
|
+
export const reexecOnNode = (nodePath, script, args, env) => {
|
|
206
|
+
const child = Object.assign({}, env, { [HOOK_NODE_REEXEC_ENV]: "1" });
|
|
207
|
+
delete child.WORKIT_HOOK_NODE_VERSION;
|
|
208
|
+
const run = spawnSync(nodePath, [script].concat(args), {
|
|
209
|
+
stdio: "inherit",
|
|
210
|
+
env: child,
|
|
211
|
+
windowsHide: true,
|
|
212
|
+
});
|
|
213
|
+
if (run.error) return null;
|
|
214
|
+
return run.status === null ? 1 : run.status;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* What a launcher does on a Node below its floor: re-run itself on a resolved
|
|
219
|
+
* Node (the child's exit code is returned), else fail open: one stderr line,
|
|
220
|
+
* and `{}` on stdout, except that a start event answers with `notice(text)` so
|
|
221
|
+
* the agent is told the hooks are off. Resolves to the exit code.
|
|
222
|
+
* @param {{ host: string, range: string, floor: number[], found: string, script: string,
|
|
223
|
+
* argv: string[], env: NodeJS.ProcessEnv, isStart: (event: unknown) => boolean,
|
|
224
|
+
* notice: (text: string) => unknown }} options
|
|
225
|
+
*/
|
|
226
|
+
export const handleBelowFloor = async (options) => {
|
|
227
|
+
if (!options.env[HOOK_NODE_REEXEC_ENV]) {
|
|
228
|
+
const hit = resolveHookNode({ env: options.env, floor: options.floor });
|
|
229
|
+
const code = hit ? reexecOnNode(hit.path, options.script, options.argv, options.env) : null;
|
|
230
|
+
if (code !== null) return code;
|
|
231
|
+
}
|
|
232
|
+
process.stderr.write(
|
|
233
|
+
`[workit] ${options.host} hook unavailable: needs Node ${options.range}, found ${options.found}\n`,
|
|
234
|
+
);
|
|
235
|
+
let early = "";
|
|
236
|
+
for await (const chunk of process.stdin) early += String(chunk);
|
|
237
|
+
let event;
|
|
238
|
+
try {
|
|
239
|
+
event = JSON.parse(early).hook_event_name;
|
|
240
|
+
} catch {
|
|
241
|
+
event = undefined;
|
|
242
|
+
}
|
|
243
|
+
process.stdout.write(
|
|
244
|
+
options.isStart(event)
|
|
245
|
+
? `${JSON.stringify(options.notice(nodeFloorNotice(options.found, options.range)))}\n`
|
|
246
|
+
: "{}\n",
|
|
247
|
+
);
|
|
248
|
+
return 0;
|
|
249
|
+
};
|