@appchy/jarvis 0.1.36 → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/bin/config-change.mjs +1 -1
  2. package/bin/jarvis.mjs +8 -12
  3. package/bin/pre-tool-use.mjs +2 -2
  4. package/bin/session-start.mjs +1 -1
  5. package/bin/stop.mjs +1 -1
  6. package/bin/user-prompt-submit.mjs +1 -1
  7. package/dist/bin.js +10104 -37174
  8. package/dist/bin.js.map +1 -1
  9. package/dist/data/backends.mjs +469 -0
  10. package/dist/data/chunk-7REP35VA.mjs +462 -0
  11. package/dist/data/chunk-AKQQC5IT.mjs +1224 -0
  12. package/dist/data/chunk-AYOJSS2F.mjs +72 -0
  13. package/dist/data/chunk-RRJ6KKYL.mjs +42 -0
  14. package/dist/data/chunk-YWSWQEJN.mjs +138 -0
  15. package/dist/data/embedders.mjs +80 -0
  16. package/dist/data/finders.mjs +35 -0
  17. package/dist/data/index.mjs +137 -0
  18. package/dist/data/linkers.mjs +83 -0
  19. package/dist/data/mcp.mjs +2624 -0
  20. package/dist/data/persistences.mjs +8 -0
  21. package/dist/data/rerankers.mjs +52 -0
  22. package/dist/data/stores.mjs +38 -0
  23. package/dist/hooks/config-change.js +7 -29
  24. package/dist/hooks/config-change.js.map +1 -1
  25. package/dist/hooks/pre-tool-use.js +129 -32
  26. package/dist/hooks/pre-tool-use.js.map +1 -1
  27. package/dist/hooks/session-start.js +72 -37
  28. package/dist/hooks/session-start.js.map +1 -1
  29. package/dist/hooks/stop.js +140 -32
  30. package/dist/hooks/stop.js.map +1 -1
  31. package/dist/hooks/user-prompt-submit.js +7 -29
  32. package/dist/hooks/user-prompt-submit.js.map +1 -1
  33. package/harness/assets/architecture-template.md +53 -0
  34. package/harness/assets/design-template.md +89 -0
  35. package/harness/assets/domains/architecture.md +43 -0
  36. package/harness/assets/domains/commercial.md +42 -0
  37. package/harness/assets/domains/design.md +42 -0
  38. package/harness/assets/domains/legal.md +43 -0
  39. package/harness/assets/domains/operations.md +39 -0
  40. package/harness/assets/domains/product.md +43 -0
  41. package/harness/assets/domains/quality.md +43 -0
  42. package/harness/assets/domains/security.md +44 -0
  43. package/harness/assets/domains/support.md +45 -0
  44. package/harness/assets/epic-template.md +85 -0
  45. package/harness/assets/feature-template.md +86 -0
  46. package/harness/assets/handoff-template.md +26 -0
  47. package/harness/assets/task-template.md +38 -0
  48. package/harness/assets/version-template.md +43 -0
  49. package/harness/harness/__init__.py +7 -0
  50. package/harness/harness/align.py +582 -0
  51. package/harness/harness/architecture.py +254 -0
  52. package/harness/harness/autonomy.py +374 -0
  53. package/harness/harness/branches.py +408 -0
  54. package/harness/harness/config.py +1482 -0
  55. package/harness/harness/coverage.py +199 -0
  56. package/harness/harness/epic.py +220 -0
  57. package/harness/harness/events.py +153 -0
  58. package/harness/harness/extend.py +99 -0
  59. package/harness/harness/frontmatter.py +218 -0
  60. package/harness/harness/gate.py +591 -0
  61. package/harness/harness/generate.py +267 -0
  62. package/harness/harness/git.py +775 -0
  63. package/harness/harness/ids.py +140 -0
  64. package/harness/harness/kickoff.py +231 -0
  65. package/harness/harness/lint.py +505 -0
  66. package/harness/harness/model.py +364 -0
  67. package/harness/harness/peers.py +187 -0
  68. package/harness/harness/product.py +29 -0
  69. package/harness/harness/registry.py +382 -0
  70. package/harness/harness/report.py +227 -0
  71. package/harness/harness/safety.py +387 -0
  72. package/harness/harness/scaffold.py +129 -0
  73. package/harness/harness/shard.py +63 -0
  74. package/harness/harness/shift.py +348 -0
  75. package/harness/harness/task.py +507 -0
  76. package/harness/harness/tree.py +258 -0
  77. package/harness/harness/version.py +305 -0
  78. package/harness/harness/wrap.py +217 -0
  79. package/harness/hooks/guard.py +259 -0
  80. package/harness/presets/appchy/PRESET.md +717 -0
  81. package/harness/presets/appchy/references/artifacts.md +539 -0
  82. package/harness/presets/appchy/references/graph.md +154 -0
  83. package/harness/presets/appchy/references/operations.md +444 -0
  84. package/harness/presets/appchy/references/research.md +216 -0
  85. package/harness/schema/work.config.schema.json +401 -0
  86. package/harness/test_work.py +5002 -0
  87. package/harness/work.py +534 -0
  88. package/package.json +34 -40
  89. package/bin/config-change.dev.mjs +0 -17
  90. package/bin/jarvis-dev.mjs +0 -30
  91. package/bin/pre-tool-use.dev.mjs +0 -23
  92. package/bin/session-start.dev.mjs +0 -17
  93. package/bin/stop.dev.mjs +0 -17
  94. package/bin/user-prompt-submit.dev.mjs +0 -17
  95. package/dev/bin.js +0 -38718
  96. package/dev/bin.js.map +0 -1
  97. package/dev/env.json +0 -1
  98. package/dev/hooks/config-change.js +0 -110
  99. package/dev/hooks/config-change.js.map +0 -1
  100. package/dev/hooks/pre-tool-use.js +0 -120
  101. package/dev/hooks/pre-tool-use.js.map +0 -1
  102. package/dev/hooks/session-start.js +0 -115
  103. package/dev/hooks/session-start.js.map +0 -1
  104. package/dev/hooks/stop.js +0 -112
  105. package/dev/hooks/stop.js.map +0 -1
  106. package/dev/hooks/user-prompt-submit.js +0 -111
  107. package/dev/hooks/user-prompt-submit.js.map +0 -1
  108. package/dist/env.json +0 -1
@@ -0,0 +1,216 @@
1
+ # Research procedure — sources + raw + take + synthesis
2
+
3
+ The canonical way to run research. Load this when a task needs external
4
+ findings (prior art, market/tech survey, an unfamiliar domain) — i.e. whenever
5
+ a `research.md` or `research/<dir>/` artifact is called for (see
6
+ [`artifacts.md`](../../../references/artifacts.md) §Conditional task-level files / §shared pool).
7
+
8
+ ## Who researches is the REPO's choice, not this skill's
9
+
10
+ `research.agent` in `work.config.json` names the subagent that runs it. **This
11
+ skill names no model and no vendor** — which model researches is a repo
12
+ preference, and baking one in here would ship a bias into every repo that
13
+ loads the plugin, exactly as naming a test command in `verify` would.
14
+
15
+ Any addressable agent is a legal value — a Codex one, a Claude one, another
16
+ family's. **A Claude researcher costs you the independent read**: its take and
17
+ the synthesis you write on top come from the same model, so their agreement
18
+ tells the founder nothing. That is a trade to make knowingly, not a rule
19
+ against it, and the way to make it knowingly is to name the researcher in
20
+ `00-report.md` so the founder can see whose agreement they are looking at.
21
+
22
+ Unset is fully supported: pick a researcher, follow the identical contract
23
+ below, and **say which one you used** in `00-report.md`, so no finding is ever
24
+ traceable to an unnamed reader.
25
+
26
+ **One researcher per run.** If you want two families' reads, run it twice
27
+ against the same `sources/` — which the layering below is built for — rather
28
+ than letting one researcher sub-contract the other mid-run. Two models sharing
29
+ one run means no snapshot has a single reader you can hold to it.
30
+
31
+ What does NOT vary, ever, is everything below this line.
32
+
33
+ ## The contract (D172 workflow; founder rule)
34
+
35
+ **The researcher delivers raw material; Claude authors the synthesis.** The two
36
+ never blur — you always keep the raw sources *and* the researcher's opinion
37
+ *separate* so the founder can judge whether anything was missed.
38
+
39
+ Four output layers, distinct owners:
40
+
41
+ | Layer | Owner | File(s) |
42
+ |---|---|---|
43
+ | **The sources themselves, verbatim** | researcher | `sources/<vendor>-<slug>.md` — one per source actually read |
44
+ | Raw, source-cited findings | researcher | `01-*.md`, `02-*.md`, … (one per sub-topic; real URLs/authors/tools) |
45
+ | The researcher's opinionated take | researcher | **`<researcher>-take.md`** — named for the model that wrote it; quarantined, labelled "not authoritative" |
46
+ | The synthesis (decisions-relevant) | **Claude** | `00-report.md` (≤30 lines) — the read-order entry point |
47
+
48
+ The researcher **never** writes `00-report.md`; Claude **never** lets its take
49
+ leak into the synthesis unattributed. This keeps the synthesis free of the
50
+ researcher's bias while preserving its opinion for the founder to weigh.
51
+
52
+ **The two lower layers are model-neutral; only the take carries a name, and it
53
+ carries the researcher's own.** `codex-take.md` when Codex researched,
54
+ `claude-take.md` when Claude did. That is not bookkeeping — it is what lets a
55
+ second model go over the *same* raw material months later and file its opinion
56
+ **beside** the first rather than on top of it:
57
+
58
+ ```
59
+ work/research/<topic>/
60
+ 00-report.md Claude's synthesis — says which researcher produced the raw layer
61
+ sources/ verbatim snapshots — belong to no model, re-readable by any
62
+ 01-*.md, 02-*.md raw findings — same
63
+ codex-take.md what Codex concluded from them
64
+ claude-take.md what Claude concluded from the same files, added later
65
+ ```
66
+
67
+ Two takes over one raw layer is a **feature**, and it is the cheap way to get a
68
+ second family's read: the sources are already fetched, so the second pass is
69
+ opinion only. Two takes that disagree is the most useful thing this directory
70
+ can hand a founder — which is only legible because each is filed under the name
71
+ of whoever wrote it.
72
+
73
+ **`sources/` IS NOT OPTIONAL, and this is the founder rule that keeps getting
74
+ dropped** (restated 2026-08-12 after an audit found it in only 7 of 18 research
75
+ dirs, with two that had decayed to a single summary file and no raw layer at
76
+ all). Every source the researcher actually reads is snapshotted verbatim into
77
+ `sources/`, so the founder and Claude can **read the original and reach their
78
+ own conclusion** rather than inheriting the researcher's. A findings file is
79
+ already one layer of interpretation; a summary of a findings file is two. When
80
+ the source disappears, changes, or turns out to have been misread, the snapshot
81
+ is the only thing that settles it.
82
+
83
+ Each snapshot opens with the same header, then the content as it was:
84
+
85
+ ```markdown
86
+ # Snapshot: <page or doc title>
87
+
88
+ Source: <exact URL>
89
+ Retrieved: <YYYY-MM-DD>
90
+
91
+ <the content, as close to verbatim as the format allows>
92
+ ```
93
+
94
+ Paraphrasing a source inside `sources/` defeats the whole point. Trim
95
+ navigation, ads and boilerplate; never trim the substance, and never rewrite it
96
+ in your own words.
97
+
98
+ **Keep the raw files CLEAN (the rule the founder cares most about).** Each
99
+ `0N-*.md` findings file holds the **actual substantive content** of each
100
+ finding — the specific facts, figures, quotes, code examples and API/tool
101
+ details the researcher actually found — **each attributed to its source URL** (or an
102
+ explicit `[knowledge-only]` tag when a claim rests on model knowledge, not a
103
+ source). It is **NOT a bare list of links**: a findings file must read as
104
+ genuine, self-contained research material — enough that the founder and Claude
105
+ can judge the substance without re-fetching the sources. And it carries **NO
106
+ recommendations, opinions, or "how our code should change" mappings** — every
107
+ one of the researcher's own takes, recommendations and code-change suggestions
108
+ goes in its `<researcher>-take.md` and nowhere else. A single merged file, or a
109
+ file of bare URLs with the real content missing, are both failures this contract
110
+ exists to prevent — the raw content and the take are **separate files**, always,
111
+ and the raw files carry the real material, not just citations.
112
+
113
+ The reason the split is worth this much insistence is the one above: a raw layer
114
+ with an opinion mixed into it is no longer re-usable by a second reader, so the
115
+ cheap second read stops being available the moment somebody merges the files.
116
+
117
+ ## How to run it
118
+
119
+ 1. **Pick the home.** `work/research/<topic>/` for shared/cross-task research;
120
+ a task-local `research/` subfolder for task-scoped work.
121
+ 2. **Forward to the researcher named by `research.agent`** (unset → pick one and
122
+ say so). Assume the runner enforces **no** file layout — the output is 100%
123
+ whatever the prompt asks for. So the prompt must carry the contract, and
124
+ **you must paste the block below VERBATIM** (filling only the bracketed
125
+ slots). **Never hand-write or paraphrase the contract from memory** — that is
126
+ exactly how the raw/take split collapses into one merged file. Load THIS file
127
+ and copy it:
128
+
129
+ ```
130
+ <structured_output_contract>
131
+ Research task — deliver RAW, source-cited findings ONLY. I (Claude) author the
132
+ synthesis; you do not. Write ONLY inside work/research/<TOPIC>/ (read-only
133
+ everywhere else — no source/config/build edits). Produce exactly these files:
134
+
135
+ - sources/<vendor>-<slug>.md (MANDATORY — one per source you actually read)
136
+ A VERBATIM snapshot of the source, so we can read the original ourselves and
137
+ reach our own conclusion. Open each with:
138
+ # Snapshot: <page or doc title>
139
+ (blank)
140
+ Source: <exact URL>
141
+ Retrieved: <YYYY-MM-DD>
142
+ then the content as it was. Trim navigation/ads/boilerplate; NEVER trim the
143
+ substance and NEVER paraphrase it — a rewritten snapshot is worthless to us.
144
+ If you could not fetch a source, say so in the findings file rather than
145
+ writing a snapshot from memory.
146
+ - 01-<subtopic>.md, 02-<subtopic>.md, … (one file per sub-topic)
147
+ RAW findings only, with the ACTUAL CONTENT of each finding — the specific
148
+ fact, figure, quote, code example or API detail — attributed to its source
149
+ URL (or an explicit [knowledge-only] tag), and pointing at the matching
150
+ sources/ file. NOT a bare list of links: write the real substance so it can
151
+ be judged without re-fetching. NO recommendations, NO opinions, NO "how our
152
+ code should change" in these files.
153
+ - <RESEARCHER>-take.md
154
+ ALL of your opinions, recommendations and code-change suggestions go here,
155
+ and ONLY here. Label it clearly as YOUR take, not authoritative. The file is
156
+ named for YOU because another model may later read the same raw files and
157
+ file its own take beside yours — so keep the layers above free of opinion,
158
+ or you have spent its read for it.
159
+
160
+ Do NOT write 00-report.md or any summary/synthesis file — I author that myself
161
+ from your raw files. Do NOT merge findings and opinion into one file. A run
162
+ that returns conclusions without sources/ has not done the job.
163
+ </structured_output_contract>
164
+ ```
165
+
166
+ Replace `<TOPIC>` with the research dir, `<RESEARCHER>` with that agent's own
167
+ short name (`codex`, `claude`, …), and `<subtopic>` with real sub-topic names;
168
+ add as many `0N-*.md` files as there are sub-topics.
169
+ 3. **Poll from the main session**, by whatever means that researcher offers.
170
+ The forwarder must not poll itself — control stays Claude-side.
171
+ 4. **Read + synthesize.** Read the raw files + the take, then author
172
+ `00-report.md` in Claude's own voice: decisions-relevant only, citing the
173
+ raw files, naming the researcher used and noting explicitly where the
174
+ synthesis diverges from its take.
175
+ 5. **Point the task's read-order at `00-report.md`**; drill into the rest only
176
+ when a question demands it (Output budgets: research is read by summary).
177
+
178
+ **Later, for a second read:** point another model at the existing `sources/` and
179
+ `0N-*.md` and ask for **only** a `<researcher>-take.md`. No re-fetching, no new
180
+ raw layer, nothing overwritten — the whole cost is one opinion. Where the two
181
+ takes disagree is worth more than either on its own.
182
+
183
+ ## Worked reference
184
+
185
+ `work/research/spec-as-source-of-truth/` is the canonical example: `01`–`05`
186
+ raw + the take quarantined + Claude's `00-report.md` synthesis.
187
+
188
+ ## What a failed run looks like
189
+
190
+ Any of these means the contract was not followed, and the fix is to re-run it
191
+ rather than to work from what came back:
192
+
193
+ - **No `sources/` directory** — conclusions with nothing to check them against.
194
+ - **`00-report.md` and nothing else.** Two dirs decayed to exactly this.
195
+ - **One merged file** carrying findings and opinion together.
196
+ - **Findings that are bare links**, or snapshots rewritten in the researcher's words.
197
+ - **A take under a generic name** (`take.md`, `researcher-take.md`) — the second
198
+ reader then has nowhere to put its own without a collision.
199
+
200
+ ## Why off-load research at all, rather than read it in-session
201
+
202
+ Two reasons, and neither names a vendor:
203
+
204
+ - **It costs the session nothing.** A researcher on its own CLI and its own
205
+ quota is usually the single biggest weekly saving in a model-routing setup —
206
+ research is token-heavy and its output is a file, not a conversation.
207
+ - **A different model family is an independent read**, which the founder can
208
+ cross-check against the synthesis. A sub-agent of the same model that writes
209
+ `00-report.md` agrees with itself for free, and that agreement means nothing.
210
+
211
+ Neither reason forbids a same-family researcher — the second one prices it.
212
+ Configure it if the first reason is what you are buying, and let the named take
213
+ file say plainly whose opinion the founder is reading.
214
+
215
+ Which one is `research.agent`. See [`../SKILL.md`](../PRESET.md) *Model routing*
216
+ for the wider tiering.
@@ -0,0 +1,401 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/appchy-ai/plugins/work.config.schema.json",
4
+ "title": "work.config.json",
5
+ "description": "Per-repo configuration for the `work` harness, committed at .claude/work.config.json. This is the ONLY configuration path: Claude Code has deliberately ignored `pluginConfigs` in project settings since v2.1.207, because a cloned repo could otherwise inject values into hook commands and MCP configs. Treat it as a security surface — a value that reaches a shell command is the same injection wearing different clothes.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string"
11
+ },
12
+ "ids": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "description": "The durable-rule id vocabulary. Ids are permanent; only their home moves.",
16
+ "properties": {
17
+ "prefix": {
18
+ "type": "string",
19
+ "pattern": "^[A-Za-z0-9]+$",
20
+ "default": "D",
21
+ "description": "The prefix this repo's rules carry — D, G, ADR. Alphanumeric only: it is compiled into a regex, so punctuation would change what counts as an id."
22
+ },
23
+ "recognised": {
24
+ "type": "array",
25
+ "items": {
26
+ "type": "string",
27
+ "pattern": "^[A-Za-z0-9]+$"
28
+ },
29
+ "default": [],
30
+ "description": "Extra prefixes accepted on READ but never generated. How a migration window stays readable without the lint going silent."
31
+ },
32
+ "undashed": {
33
+ "type": "boolean",
34
+ "default": false,
35
+ "description": "Read `D12` as well as `D-12`. Off by default: a short prefix collides with ordinary data — `priority: P0` was read as a citation to a rule `P-0` that does not exist. Turn on only where an interface really publishes undashed ids."
36
+ },
37
+ "foreign": {
38
+ "type": "array",
39
+ "items": {
40
+ "type": "string"
41
+ },
42
+ "default": [],
43
+ "description": "Sibling repos whose ids may legally appear here, e.g. \"acme D-147\". Empty by default: a fresh repo has no siblings, and shipping somebody else's repo names would silently excuse a real dangling citation in every consumer."
44
+ }
45
+ }
46
+ },
47
+ "domains": {
48
+ "type": "object",
49
+ "additionalProperties": false,
50
+ "properties": {
51
+ "order": {
52
+ "type": "array",
53
+ "items": {
54
+ "type": "string"
55
+ },
56
+ "default": [
57
+ "product",
58
+ "architecture",
59
+ "design",
60
+ "quality",
61
+ "security",
62
+ "operations",
63
+ "support",
64
+ "commercial",
65
+ "legal"
66
+ ],
67
+ "description": "Reading order of the org-domain map, not alphabetical. A domain not on the list still scans; it sorts last."
68
+ }
69
+ }
70
+ },
71
+ "spine": {
72
+ "type": "object",
73
+ "additionalProperties": false,
74
+ "description": "The conventions a plan must fit, read before planning.",
75
+ "properties": {
76
+ "standards": {
77
+ "type": [
78
+ "string",
79
+ "null"
80
+ ],
81
+ "default": null,
82
+ "description": "Repo-relative path to the standards doc, or null."
83
+ },
84
+ "conventions": {
85
+ "type": "array",
86
+ "items": {
87
+ "type": "string"
88
+ },
89
+ "default": [],
90
+ "description": "Repo-relative paths to convention docs, nearest first."
91
+ }
92
+ }
93
+ },
94
+ "plans": {
95
+ "type": "object",
96
+ "additionalProperties": false,
97
+ "properties": {
98
+ "dir": {
99
+ "type": [
100
+ "string",
101
+ "null"
102
+ ],
103
+ "default": null,
104
+ "description": "Repo-relative directory holding approved plan files, or null. `work.py plan` records a POINTER into it, never the plan body. Null is a full mode, not a missing setting: with no directory configured `plan` requires an explicit --file, so a repo that deliberately keeps no plans directory is never handed a pointer into one."
105
+ }
106
+ }
107
+ },
108
+ "product": {
109
+ "type": "object",
110
+ "additionalProperties": false,
111
+ "properties": {
112
+ "tiers": {
113
+ "type": "string",
114
+ "enum": [
115
+ "flat"
116
+ ],
117
+ "default": "flat",
118
+ "description": "`flat` — a feature is product/<feature>.md and its bare name is its whole address. The only shape implemented today; the key exists so a repo that grows an app tier has somewhere to say so rather than forking."
119
+ }
120
+ }
121
+ },
122
+ "instructions": {
123
+ "type": "object",
124
+ "additionalProperties": false,
125
+ "description": "How this repo's guidance is composed. The harness ships an ENGINE (the tree, the ids, the buckets, the delivery gate — what work.py enforces) and, separately, a PRESET: one organisation's methodology on top of it. Contradict the engine and the tool refuses you; contradict the preset and you have merely chosen to work differently. Shipping them fused made every consumer inherit one org's opinions as though they were mechanics. This is the shareable-config pattern — eslint's engine vs eslint-config-*: the engine never enumerates its consumers, and whoever holds an opinion publishes it themselves.",
126
+ "properties": {
127
+ "preset": {
128
+ "type": [
129
+ "string",
130
+ "null"
131
+ ],
132
+ "default": null,
133
+ "description": "A shipped preset by name, a repo-relative path to your own, or null for the bare engine. NULL BY DEFAULT: this shipped naming the author's own preset, so a repo that installed the harness and said nothing inherited one organisation's methodology as though it came with the mechanics. A preset is somebody's opinion, and whose opinion runs in a repo is that repo's call — the engine ships, the methodology is one key away."
134
+ },
135
+ "extend": {
136
+ "type": "array",
137
+ "items": {
138
+ "type": "string"
139
+ },
140
+ "default": [],
141
+ "description": "Repo-relative files layered ON TOP, in order. Later wins on conflict: engine < preset < extend[0] < extend[1] … Layering rather than named section overrides, deliberately — a slug API would owe every consumer a stable set of headings forever, so renaming a section would break repos that never asked to be coupled to our table of contents."
142
+ },
143
+ "skill": {
144
+ "type": [
145
+ "string",
146
+ "null"
147
+ ],
148
+ "description": "What a session runs to load this repo's method — the line every kickoff prompt opens with. Null for a prompt that names none."
149
+ }
150
+ }
151
+ },
152
+ "tags": {
153
+ "type": "object",
154
+ "additionalProperties": false,
155
+ "properties": {
156
+ "allowed": {
157
+ "type": "array",
158
+ "items": {
159
+ "type": "string"
160
+ },
161
+ "default": [],
162
+ "description": "Area tags a task's `tags:` field may carry, for grouping the board by capability or market. EMPTY MEANS NO VOCABULARY IS CONFIGURED, and then any tag is accepted — name a list and it becomes closed, so an unknown tag is refused against it. It is config rather than a shipped list because an area vocabulary is the most repo-specific thing a task carries: the tuple that used to live in the payload shipped one consumer's product names to every other repo AND refused their own. Compare `code:`, whose vocabulary is DERIVED from the systems in work/architecture/*.md — a fact that can be derived is never written, and this one cannot be."
163
+ }
164
+ }
165
+ },
166
+ "research": {
167
+ "type": "object",
168
+ "additionalProperties": false,
169
+ "properties": {
170
+ "agent": {
171
+ "type": [
172
+ "string",
173
+ "null"
174
+ ],
175
+ "default": null,
176
+ "description": "The subagent that runs external research for this repo — a Codex agent such as \"codex:codex-rescue\", a Claude one, or anything else the session can address. NAMED HERE RATHER THAN IN THE SKILL because which model researches is a repo's own preference, and hardcoding one would ship a bias — the same reason `verify` names no test command. The PROCEDURE and its output contract (verbatim sources/ + raw findings + a take filed under the researcher's own name + Claude's synthesis) are the skill's and do not vary. A Claude agent is legal, with the trade stated openly: it is not an independent read, so its take and the synthesis agree with each other for free. Null is fully supported: the session picks a researcher, follows the same contract, and SAYS which one it used, so a finding is never traceable to an unnamed reader."
177
+ }
178
+ }
179
+ },
180
+ "verify": {
181
+ "type": "object",
182
+ "additionalProperties": {
183
+ "type": "string"
184
+ },
185
+ "default": {},
186
+ "description": "Commands run to verify work, by name — e.g. {\"test\": \"<this repo's test command>\"}. Free-form because toolchains differ, and naming one here would ship a bias. EXECUTED by `work verify` and required by the completion gate: a task cannot reach complete/ without a passing record. Each is split with shlex and run with shell=False, so shell operators (&&, ||, ;, |, >, $(), backticks) are REFUSED at load rather than passed to the first binary as literal arguments — put the composition in a script and call that."
187
+ },
188
+ "autonomy": {
189
+ "type": "object",
190
+ "additionalProperties": false,
191
+ "description": "What an unattended run may do on its own. The harness runs on a schedule, so the decision boundary has to be a value rather than a habit.",
192
+ "properties": {
193
+ "ceiling": {
194
+ "type": "integer",
195
+ "minimum": 0,
196
+ "maximum": 3,
197
+ "default": 2,
198
+ "description": "The highest task `tier:` an unattended run acts on alone. 0 reversible and local · 1 ordinary change behind existing tests · 2 new behaviour or a reversible migration · 3 irreversible, or it touches money, secrets, personal data or the law. Default 2: the shift does ordinary work and parks the rest with `work ask`. 3 means \"decide everything\" and is deliberately never the shipped default."
199
+ },
200
+ "lease_minutes": {
201
+ "type": "integer",
202
+ "minimum": 1,
203
+ "default": 240,
204
+ "description": "How long one instance's claim on a task holds before another may take it. Long enough that real work finishes inside it; short enough that a crashed shift's task is takeable the same day."
205
+ }
206
+ }
207
+ },
208
+ "graph": {
209
+ "type": "object",
210
+ "additionalProperties": false,
211
+ "properties": {
212
+ "mcp": {
213
+ "type": [
214
+ "string",
215
+ "null"
216
+ ],
217
+ "default": null,
218
+ "description": "The MCP server serving this repo's governance↔code graph, or null. Null is fully supported: the planning loop degrades to grep-wide and says so. It never implies a complete map it did not get."
219
+ }
220
+ }
221
+ },
222
+ "session": {
223
+ "type": "object",
224
+ "additionalProperties": false,
225
+ "description": "Starting the NEXT session — the wrap's last act. A SEPARATE key from graph.mcp even where the same server answers both: serving a graph and starting a session are different capabilities, and a repo can have either without the other. Riding on the graph's key would mean a repo that installed a map silently gained the power to spawn sessions.",
226
+ "properties": {
227
+ "mcp": {
228
+ "type": [
229
+ "string",
230
+ "null"
231
+ ],
232
+ "default": null,
233
+ "pattern": "^[A-Za-z0-9_-]+$",
234
+ "description": "The MCP server whose tool opens a session in this repo, or null. Null is the default and a fully supported mode: `work kickoff` prints the prompt and a person carries it across, which is what the wrap always did. The value is composed into `mcp__<server>__<tool>` and is validated as a plain name for that reason."
235
+ },
236
+ "tool": {
237
+ "type": [
238
+ "string",
239
+ "null"
240
+ ],
241
+ "pattern": "^[A-Za-z0-9_-]+$",
242
+ "description": "The tool on that server that starts the session. NO DEFAULT, deliberately: a default here would be one vendor's tool name compiled into a repo-agnostic payload and shipped to every consumer as though it were a fact about how sessions start. Naming a server therefore obliges you to name its tool — session.mcp without session.tool is refused at load. The tool must take a `prompt` and a `cwd`, and its answer must say whether the started session outlives the caller."
243
+ }
244
+ }
245
+ },
246
+ "hooks": {
247
+ "type": "object",
248
+ "additionalProperties": false,
249
+ "description": "The hook points this plugin owns, and what a repo may do to each. The declaration in the plugin's hooks.json is the plugin's and a consumer cannot change it; what a consumer needs is to change what is SAID there. Keyed by Claude Code's own event names rather than by what the harness does at each, so an installer writes the key it already has a name for.",
250
+ "properties": {
251
+ "session_start": {
252
+ "type": "object",
253
+ "additionalProperties": false,
254
+ "description": "The standing obligations a fresh session is given.",
255
+ "properties": {
256
+ "enabled": {
257
+ "type": "boolean",
258
+ "default": true,
259
+ "description": "Whether the harness says its OWN lines here. false silences them; paired with `extend` that is how a repo REPLACES this hook's output rather than adding to it, and alone it switches the point off."
260
+ },
261
+ "extend": {
262
+ "type": "array",
263
+ "items": {
264
+ "type": "string"
265
+ },
266
+ "default": [],
267
+ "description": "Commands the harness runs at this point, whose output is folded in after its own. Run with shell=False after a shlex split and never interpolated, so shell operators are refused at load — put any composition in a script and name that. A command that fails, is missing, or runs long is REPORTED in place of its lines, never silently dropped."
268
+ }
269
+ }
270
+ },
271
+ "stop": {
272
+ "type": "object",
273
+ "additionalProperties": false,
274
+ "description": "The end-of-turn wrap reminder. This is the HOOK; `wrap.at_percent` is when it fires, and they are separate because a repo can want the threshold without the harness's wording, or the reverse.",
275
+ "properties": {
276
+ "enabled": {
277
+ "type": "boolean",
278
+ "default": true,
279
+ "description": "Whether the harness says its OWN lines here. false silences them; paired with `extend` that is how a repo REPLACES this hook's output rather than adding to it, and alone it switches the point off."
280
+ },
281
+ "extend": {
282
+ "type": "array",
283
+ "items": {
284
+ "type": "string"
285
+ },
286
+ "default": [],
287
+ "description": "Commands the harness runs at this point, whose output is folded in after its own. Run with shell=False after a shlex split and never interpolated, so shell operators are refused at load — put any composition in a script and name that. A command that fails, is missing, or runs long is REPORTED in place of its lines, never silently dropped."
288
+ }
289
+ }
290
+ }
291
+ }
292
+ },
293
+ "git": {
294
+ "type": "object",
295
+ "additionalProperties": false,
296
+ "description": "Whether a board write lands in git. OFF BY DEFAULT: this harness is shared, and a repo that never asked for it must not start committing on its own. Where it is on, the commit's trailers are the activity record and no work/.events.jsonl is written.",
297
+ "properties": {
298
+ "commit": {
299
+ "type": "boolean",
300
+ "default": false,
301
+ "description": "Commit every board write, the moment it happens. No debounce and no window — the window is the bug: an item created inside one existed in no git history anywhere."
302
+ },
303
+ "push": {
304
+ "type": "boolean",
305
+ "default": false,
306
+ "description": "Pull before, push after. Needs `commit`. Off separately so a repo with no shared origin still gets the half that makes a write unloseable."
307
+ },
308
+ "remote": {
309
+ "type": "string",
310
+ "default": "origin",
311
+ "description": "The remote a board write is pushed to."
312
+ },
313
+ "paths": {
314
+ "type": "array",
315
+ "items": {
316
+ "type": "string"
317
+ },
318
+ "minItems": 1,
319
+ "default": [
320
+ "work"
321
+ ],
322
+ "description": "What a board write commits. Repo-relative, and refused if it could point outside the repo — these become pathspecs on a real commit. A hold (.claim) is never committed whatever this says."
323
+ }
324
+ }
325
+ },
326
+ "coverage": {
327
+ "type": "object",
328
+ "additionalProperties": false,
329
+ "properties": {
330
+ "shard": {
331
+ "type": "string",
332
+ "default": ".work/coverage",
333
+ "description": "Where runners drop coverage shards. Gitignored on purpose — evidence is a fresh run, not a file somebody checked in."
334
+ }
335
+ }
336
+ },
337
+ "hygiene": {
338
+ "type": "object",
339
+ "additionalProperties": false,
340
+ "properties": {
341
+ "review": {
342
+ "type": [
343
+ "string",
344
+ "null"
345
+ ],
346
+ "default": null,
347
+ "description": "The command this repo runs for its bug pass, or null to skip it. NULL BY DEFAULT: the shipped defaults used to name two commands, which was wrong twice over — nothing read the keys, so a repo that set its own was silently ignored, while the prose named those two to every consumer regardless of what it had. Where it is set, the SessionStart block names it; where it is null there is no pass and the harness invents none."
348
+ },
349
+ "simplify": {
350
+ "type": [
351
+ "string",
352
+ "null"
353
+ ],
354
+ "default": null,
355
+ "description": "The command this repo runs for its quality pass, or null to skip it. NULL BY DEFAULT: the shipped defaults used to name two commands, which was wrong twice over — nothing read the keys, so a repo that set its own was silently ignored, while the prose named those two to every consumer regardless of what it had. Where it is set, the SessionStart block names it; where it is null there is no pass and the harness invents none."
356
+ }
357
+ }
358
+ },
359
+ "wrap": {
360
+ "type": "object",
361
+ "additionalProperties": false,
362
+ "description": "Finishing a session cleanly before the context window runs out. OFF unless a repo opts in: the Stop hook behind it speaks up at the end of a turn, and a hook that interrupts somebody's session is theirs to switch on — the same reasoning that keeps hooks/guard.py inert.",
363
+ "properties": {
364
+ "at_percent": {
365
+ "type": [
366
+ "integer",
367
+ "null"
368
+ ],
369
+ "minimum": 1,
370
+ "maximum": 99,
371
+ "default": null,
372
+ "description": "Share of the context window past which the session is reminded to wrap up — write the handoff, bring task/epic docs current, move statuses, and leave a kickoff prompt for the next session. Null is off. 100 is refused along with 0: a reminder arriving once the window is full has nowhere left to write the handoff it asks for."
373
+ },
374
+ "context_tokens": {
375
+ "type": [
376
+ "integer",
377
+ "null"
378
+ ],
379
+ "minimum": 1,
380
+ "default": null,
381
+ "description": "The window at_percent is measured against. NO DEFAULT, and at_percent requires it: a hook cannot ask Claude Code how large the context is, so any shipped number is a guess about somebody else's session. 200000 nagged a long-context consumer from its first turn; 1000000, which replaced it, would let a standard 200k session reach 100% having been told it was at 20%. Both fail silently. State the window you actually run. Usage itself is never estimated — it is read from the API's own `usage` record, so the percentage is exact."
382
+ },
383
+ "command": {
384
+ "type": [
385
+ "string",
386
+ "null"
387
+ ],
388
+ "description": "What to run to wrap a session up, named in the reminder. Null to remind without naming a command."
389
+ }
390
+ }
391
+ },
392
+ "skip_dirs": {
393
+ "type": "array",
394
+ "items": {
395
+ "type": "string"
396
+ },
397
+ "default": [],
398
+ "description": "Extra generated or vendored directories to skip, beyond the built-in rule (any dot-directory except .claude, plus the standard build dirs). Scanning a generated cache mints phantom test bindings — one snapshot dir of 1,521 files did exactly that."
399
+ }
400
+ }
401
+ }