@mrciphersmith/keryx 0.3.122 → 0.3.123

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.122",
3
+ "version": "0.3.123",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -54,6 +54,7 @@
54
54
  "check": "bun run lint && bun run typecheck && bun run typecheck:scripts && bun test",
55
55
  "check:core": "bun run lint && bun run typecheck && bun run typecheck:scripts && bun run test:core",
56
56
  "docs:build": "mkdocs build --strict",
57
+ "docs:check": "bun run check:doc-links && bun run check:retired-spellings && bun test src/cli-reference-coverage.test.ts src/standard/commands-by-task.test.ts src/standard/help-groups.test.ts",
57
58
  "check:doc-links": "bun scripts/check-doc-links.ts",
58
59
  "check:retired-spellings": "bun scripts/check-retired-cli-spellings.ts",
59
60
  "baseline:review-precision": "bun scripts/review-precision-baseline.ts",
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { revisionOperand } from "../lib/git-revision";
5
- import { gitCmd, gitHead, readProvenance } from "../sync/provenance";
5
+ import { gitCmd, readProvenance } from "../sync/provenance";
6
6
 
7
7
  // W7 AC4 follow-up (flow 304 T8) — a real, previously-latent defect found
8
8
  // while wiring up the `modified` bucket below: `gitCmd`/`gitCmdResult`
@@ -35,7 +35,7 @@ import { gitCmd, gitHead, readProvenance } from "../sync/provenance";
35
35
  // splitting: an ordinary entry is `XY PATH\0`, but a rename/copy (X or Y is
36
36
  // `R`/`C`) is `XY PATH\0ORIG_PATH\0` — TWO NUL-terminated fields, not one —
37
37
  // which `categorizeStatusEntries` below accounts for.
38
- function gitStatusPorcelain(cwd: string): Promise<string | null> {
38
+ export function gitStatusPorcelain(cwd: string): Promise<string | null> {
39
39
  return new Promise((resolve) => {
40
40
  try {
41
41
  const child = spawn("git", ["status", "--porcelain=v1", "-z"], { cwd, stdio: ["ignore", "pipe", "ignore"] });
@@ -258,7 +258,10 @@ async function committedChangedFiles(
258
258
  * The structured staleness check (AFC-10). Never throws — a git failure is
259
259
  * reported as `status: "unknown"` with a reason, not thrown and not "fresh".
260
260
  */
261
- export async function checkGraphStaleness(cwd: string): Promise<StalenessCheck> {
261
+ export async function checkGraphStaleness(
262
+ cwd: string,
263
+ statusObservation?: Promise<string | null>,
264
+ ): Promise<StalenessCheck> {
262
265
  const nodesStat = await stat(nodesJsonlPath(cwd)).catch(() => null);
263
266
  if (!nodesStat) {
264
267
  return { status: "stale", reasons: ["graph has not been built yet (no nodes.jsonl)"] };
@@ -303,19 +306,30 @@ export async function checkGraphStaleness(cwd: string): Promise<StalenessCheck>
303
306
  // `unknown` (a weaker claim — "not verified", not "confirmed stale") rather
304
307
  // than staying silently fresh.
305
308
  let provenanceMissing = false;
306
- const head = await gitHead(cwd);
309
+ // Independent local probes overlap. The shared status belongs to this call
310
+ // only; standalone checks always obtain a new observation.
311
+ const [commit, prefix, porcelain, provenance] = await Promise.all([
312
+ gitCmd(cwd, ["rev-parse", "HEAD"]),
313
+ gitCmd(cwd, ["rev-parse", "--show-prefix"]),
314
+ statusObservation ?? gitStatusPorcelain(cwd),
315
+ readProvenance(cwd, "gdgraph"),
316
+ ]);
317
+ const head = commit ? { commit } : null;
307
318
  // T19 finding 3: `--show-prefix` is the project root's path relative to the
308
319
  // git root (empty string AT the git root) — needed both here (to scope a
309
320
  // commit-range diff to real source, excluding the graph's own bookkeeping)
310
321
  // and below (the working-tree `.metaproject/` exclusion). Fetched once and
311
322
  // reused, so this stays within the module's stated "one or two git calls"
312
323
  // budget rather than asking twice.
313
- const rootPrefix = head !== null ? ((await gitCmd(cwd, ["rev-parse", "--show-prefix"])) ?? "") : "";
324
+ const rootPrefix = prefix ?? "";
325
+ if (prefix === null) {
326
+ gitFailed = true;
327
+ reasons.push("git rev-parse --show-prefix failed");
328
+ }
314
329
  if (head === null) {
315
330
  gitFailed = true;
316
331
  reasons.push("git rev-parse HEAD failed (not a git repository, or git is unavailable)");
317
332
  } else {
318
- const provenance = await readProvenance(cwd, "gdgraph");
319
333
  if (provenance) {
320
334
  if (provenance.commit !== head.commit) {
321
335
  const changedFiles = await committedChangedFiles(cwd, provenance.commit, head.commit, rootPrefix);
@@ -343,7 +357,6 @@ export async function checkGraphStaleness(cwd: string): Promise<StalenessCheck>
343
357
  }
344
358
 
345
359
  // --- untracked / deleted / renamed / modified ------------------------------
346
- const porcelain = await gitStatusPorcelain(cwd);
347
360
  if (porcelain === null) {
348
361
  gitFailed = true;
349
362
  reasons.push("git status failed");
@@ -14,6 +14,15 @@ with context. It is NOT a git hook — the `gdskills` post-commit hook only runs
14
14
  `skills verify --all --dry-run` as an advisory staleness signal and never
15
15
  mutates.
16
16
 
17
+ ## Project rules — read first, then say so
18
+
19
+ Before writing code or tests, run `keryx skills resolve-context --files <files>
20
+ --for implement|test`. The `must_read` list puts project rules and project
21
+ skills ahead of the bundled ones. Read every entry, then return
22
+ `project_context_applied` with each path and its `sha256`. A missing entry is a
23
+ warning by default and a refusal under `gdskills.projectContext.enforcement:
24
+ block`.
25
+
17
26
  ## Verify — cheap, inline, before you trust a skill
18
27
 
19
28
  Whoever is about to rely on a project-skill verifies it first:
@@ -23,6 +23,12 @@ Before creating or editing a skill, ask and confirm:
23
23
  is an installed mirror, regenerated by `keryx init`/`keryx update` — never
24
24
  edit it directly, edits there are overwritten on the next run.
25
25
  - Project-local skill: `.metaproject/project-skills/<skill-name>/`.
26
+ - Project rule: `.metaproject/rules/project/<rule>.mdc`, brought in with
27
+ `keryx skills import --rules <dir>` (`--link` reads a folder in place). Project
28
+ rules and project implementation skills are resolved first, bundled ones
29
+ second (`keryx skills resolve-context`). A project skill supplements the
30
+ built-in `task-implementer` and `tests-creator`; it never takes a bundled
31
+ skill's name.
26
32
 
27
33
  ## Required Source Layout
28
34
  - `SKILL.md` — canonical source of truth, the shared build used by every runtime
@@ -143,9 +143,8 @@ it already tried. The flow package does.
143
143
  or close the task (`keryx flow task done <id> <Tn>`). Re-dispatching
144
144
  over an unresolved attempt is how the same work gets done twice.
145
145
 
146
- `keryx flow next` also lists every OTHER not-done task with an unresolved
147
- attempt, under `unresolved`: parallel dispatches that never reported back.
148
- Resolve them the same way before assuming the flow is idle.
146
+ `keryx flow next` also lists, under `unresolved`, every OTHER not-done task with an
147
+ unresolved attempt (parallel dispatches that never reported back); resolve them the same way.
149
148
 
150
149
  5. Before dispatching a worker for that task, record the attempt:
151
150
 
@@ -164,10 +163,8 @@ it already tried. The flow package does.
164
163
  keryx review loop --flow <id> --task <Tn>
165
164
  ```
166
165
 
167
- A non-zero exit means the same finding has recurred or two consecutive
168
- rounds produced identical output. Go straight to the re-planning step.
169
- Do not spend the remaining attempts on the same approach because the
170
- budget has some left — that is the failure this check exists to catch.
166
+ A non-zero exit means the same finding recurred or two consecutive rounds
167
+ produced identical output: go straight to the re-planning step, whatever budget is left.
171
168
  8. If the flow is `blocked`, read the blocking reason from `journal.md`,
172
169
  resolve or escalate it, then `keryx flow unblock <id>`.
173
170
  4. If the user wants a new flow, continue at 0.1.
@@ -323,6 +320,8 @@ auto-commit off (task-implementer's `automation.auto_commit: false`). A
323
320
  `subagent-dispatch` has no `automation` field, so the setting travels as the
324
321
  explicit constraint below — never as the worker's default, which is `true`.
325
322
 
323
+ Before every `test`, `implement` and fix dispatch, run `keryx skills resolve-context --for test|implement --files <the task's target files, comma-joined> --task-type <type> --task-text '<the task's title and description>' --out .metaproject/flows/<dir>/resolved/<Tn>.<for>.json` (a fix dispatch re-resolves on the new head) and pass that path as `resolved_context_file`: a `context_refs` entry plus the constraint below. Write the text single-quoted (a `'` inside it becomes `'\''`): double quotes let the shell run any backtick or `$(...)` a task description contains. `--task-text` (the harness and MCP field is `task_text`) is what lets a keyword of a document declared in `references.json` match; without it only globs promote. A promoted entry comes back as `must_read` with reason `condition:glob:<g>` or `condition:keyword:<k>`, and its `project_context_applied` item must carry the `path`, the `sha256` of the whole file as pinned, the declared `anchor`, a `summary` (at least 40 characters, in the worker's own words, reusing at least three distinct words of five or more letters from the anchored section) and `how` `read` or `followed`, never `n/a`. Before accepting the reply, run `keryx skills contracts validate <result.json> --schema task-implementer-output|tests-creator-output --resolved <that file>`; under `gdskills.projectContext.enforcement: block` a miss is a refusal, under `warn` journal it.
324
+
326
325
  Dispatch payload, bound to the flow (map `target_skill` from the routing table):
327
326
 
328
327
  ```json
@@ -337,12 +336,14 @@ Dispatch payload, bound to the flow (map `target_skill` from the routing table):
337
336
  "context_refs": [
338
337
  { "path": ".metaproject/flows/<dir>/context.md", "kind": "context", "exists": true },
339
338
  { "path": ".metaproject/flows/<dir>/plan.md", "kind": "plan", "exists": true },
340
- { "path": ".metaproject/flows/<dir>/acceptance-criteria.md", "kind": "custom", "exists": true }
339
+ { "path": ".metaproject/flows/<dir>/acceptance-criteria.md", "kind": "custom", "exists": true },
340
+ { "path": ".metaproject/flows/<dir>/resolved/<Tn>.<for>.json", "kind": "custom", "exists": true }
341
341
  ],
342
342
  "files_to_read": ["<only files gdgraph/gdctx narrowed to>"],
343
343
  "constraints": [
344
344
  "Never edit flow.json.",
345
345
  "Never edit frozen acceptance criteria.",
346
+ "resolved_context_file: .metaproject/flows/<dir>/resolved/<Tn>.<for>.json — read its must_read entries and cite each in project_context_applied; an entry promoted by a condition also needs its anchor and a summary.",
346
347
  "Return a subagent-result; first line must be STATUS:.",
347
348
  "Follow rules/core/git-concurrency.mdc: never git stash; never git add -A.",
348
349
  "Root: <worktree_path>. Before the first write run `cd <worktree_path> && pwd && git branch --show-current`; run every git command as `git -C <worktree_path>`.",
@@ -359,9 +360,8 @@ Dispatch payload, bound to the flow (map `target_skill` from the routing table):
359
360
  `DONE_WITH_CONCERNS`), then mark task progress. It commits exactly those
360
361
  `changed_files` from the worker's `subagent-result` that git still shows as
361
362
  changed, never `-A`/`--all`/`.` (`rules/core/git-concurrency.mdc` rule 4 and
362
- "Reporting Back"). It skips cleanly when none remain. That is expected, not an
363
- error, for a worker that reported no files (review, context, docs) or that
364
- committed on its own (tests-creator's stubs):
363
+ "Reporting Back"). It skips cleanly when none remain (expected for a worker that
364
+ reported no files or committed on its own, like tests-creator's stubs):
365
365
 
366
366
  ```bash
367
367
  set -- <each changed_files path, single-quoted: 'src/a.ts' 'docs/b c.md'> # may be none; an unquoted path with a space splits and is skipped
@@ -746,8 +746,10 @@ FOR wave_index, wave_tasks in enumerate(WAVES):
746
746
 
747
747
  keryx job step <job-name> tests-creator --status in-progress # wave 1 only
748
748
  # Step A — tests-creator (MANDATORY, run first)
749
- Dispatch one tests-creator per task in this wave, in a SINGLE turn (parallel).
750
- Wait for ALL of them. Collect TEST_SPECS[task_id] from each response.
749
+ keryx skills resolve-context --for test --files <task target_files, comma-joined> --task-type <task_type> --task-text '<task title and description, each ' written as '\''>' --out <JOBS_ROOT>/<job-name>/results/<task_id>.test.resolved.json
750
+ Dispatch one tests-creator per task in this wave, in a SINGLE turn (parallel), each carrying resolved_context_file.
751
+ Wait for ALL of them. Collect TEST_SPECS[task_id] from each response, then per result:
752
+ keryx skills contracts validate <task_id>-tests.json --schema tests-creator-output --resolved <that resolved file>
751
753
  keryx job step <job-name> tests-creator --status completed # last wave only
752
754
 
753
755
  # Parallel safety check, before Step B:
@@ -755,9 +757,10 @@ FOR wave_index, wave_tasks in enumerate(WAVES):
755
757
 
756
758
  keryx job step <job-name> implement --status in-progress # wave 1 only
757
759
  # Step B — task-implementer (after all test stubs are committed)
760
+ keryx skills resolve-context --for implement ... --out <JOBS_ROOT>/<job-name>/results/<task_id>.implement.resolved.json
758
761
  Dispatch one task-implementer per task in this wave, in a SINGLE turn (parallel),
759
- each carrying test_case_specs: TEST_SPECS[task_id].
760
- Wait for ALL of them.
762
+ each carrying test_case_specs: TEST_SPECS[task_id] and resolved_context_file (an entry promoted by a declared condition, reason condition:glob:<g> or condition:keyword:<k>, needs its project_context_applied item to carry path, sha256 of the whole file, the declared anchor, a summary of 40+ characters in the worker's own words, and how read|followed, never n/a).
763
+ Wait for ALL of them; per result: keryx skills contracts validate <result.json> --schema task-implementer-output --resolved <that resolved file>
761
764
 
762
765
  Read each result's STATUS line:
763
766
  DONE / DONE_WITH_CONCERNS → accept it (record every concern)
@@ -789,6 +792,7 @@ Task({
789
792
  - package_manager: <pm>
790
793
  - run_command: <runner>
791
794
  - context_path: <JOBS_ROOT>/<job-name>/ai/context.md
795
+ - resolved_context_file: <JOBS_ROOT>/<job-name>/results/<task_id>.test.resolved.json
792
796
 
793
797
  ## Required response
794
798
  Begin with STATUS: <STATUS>. Return the test_case_specs for this task and
@@ -819,6 +823,7 @@ Task({
819
823
  - issue_number: <N>
820
824
  - job_name: <job-name>
821
825
  - context_path: <JOBS_ROOT>/<job-name>/ai/context.md
826
+ - resolved_context_file: <JOBS_ROOT>/<job-name>/results/<task_id>.implement.resolved.json
822
827
 
823
828
  ## Automation (task-implementer input `automation`)
824
829
  - auto_commit: <implementer_settings.auto_commit> # false: do not commit; report exact paths
@@ -1114,10 +1119,8 @@ keryx review tier --scope <scope> --diff-lines <n> --findings <n> [--security] [
1114
1119
  ```
1115
1120
 
1116
1121
  Paste the `model` block it prints into that dispatch. `model_strategy: "current"` is
1117
- gone: it meant "do not switch models", which is exactly the behaviour this command
1118
- replaced. The command names no model — it ranks what the provider reports at runtime
1119
- and, when it cannot rank anything, prints `inherit: true`, which means the dispatch
1120
- runs on the session model. That is a correct answer, not a failure.
1122
+ gone. The command names no model: it ranks what the provider reports at runtime and,
1123
+ when it cannot, prints `inherit: true` — the dispatch runs on the session model, a correct answer.
1121
1124
 
1122
1125
  **Step 6 — run `review-orchestrator` inline, in this main session.** Load
1123
1126
  `skills/gdskills/review/review-orchestrator/SKILL.md` yourself; do **not** wrap it in a
@@ -1159,10 +1162,9 @@ keryx review ingest --report <path/to/review-report.md> --ref <feature-branch> \
1159
1162
  --spent <usd-so-far> --outstanding <subagents in flight>
1160
1163
  ```
1161
1164
 
1162
- An unrecognised option is **refused, not ignored** — a silently dropped flag writes
1163
- nothing and still reports success. `--refuted` carries findings this round raised and
1164
- then dismissed; without it the package keeps only the survivors of an unlogged triage.
1165
- A first ingest without `--premises` holds nothing; a re-ingest carries earlier holds.
1165
+ An unrecognised option is **refused, not ignored**. `--refuted` carries findings this round
1166
+ raised and then dismissed; without it the package keeps only the survivors of an unlogged
1167
+ triage. A first ingest without `--premises` holds nothing; a re-ingest carries earlier holds.
1166
1168
 
1167
1169
  **Findings are the canonical shape.** One vocabulary, everywhere in this skill:
1168
1170
 
@@ -1278,15 +1280,11 @@ cumulative ever-correct is **0.847**
1278
1280
  ([arXiv:2607.24604](https://arxiv.org/abs/2607.24604)). Other agent tools
1279
1281
  hardcode the same bound (`max_reflections = 3`).
1280
1282
 
1281
- The bound is a ceiling, not a target. Repetition ends the loop earlier and
1282
- **regardless of remaining iterations** — a counter cannot tell "converging
1283
- slowly" from "stuck", and an agent emitting the identical failing output three
1284
- times spends the whole budget before anything notices.
1283
+ The bound is a ceiling, not a target. Repetition ends the loop earlier, **regardless of
1284
+ remaining iterations**: a counter cannot tell "converging slowly" from "stuck".
1285
1285
 
1286
- **A finding leaves this loop by being dispositioned, never by being absent.** The
1287
- previous version of this section recomputed "unresolved" as whatever the next round
1288
- still reported — so a finding the next reviewer simply did not look at was recorded as
1289
- fixed. That is absence-as-evidence, and the completion gate refuses it.
1286
+ **A finding leaves this loop by being dispositioned, never by being absent.** One the next
1287
+ reviewer did not look at is not fixed: absence-as-evidence, which the completion gate refuses.
1290
1288
 
1291
1289
  ```
1292
1290
  UNRESOLVED_FINDINGS = OPEN from step 2.6 # never a HELD finding (Classify, 2.6.1)
@@ -1306,9 +1304,10 @@ FOR iteration in [1, 2, 3]:
1306
1304
  previously_unresolved: <findings that were in UNRESOLVED_FINDINGS last iteration but still present>
1307
1305
  → Prefix: "These specific findings were NOT fixed in iteration <N-1>: [list]"
1308
1306
 
1309
- 3. Launch task-implementer with the fix prompt (subagent_type: "general-purpose"),
1307
+ 3. Re-run `keryx skills resolve-context --for implement` on the fix files (with `--task-text '<fix task description, each ' written as '\''>'` and a fresh `--out`), then launch task-implementer
1308
+ with the fix prompt and `resolved_context_file` (subagent_type: "general-purpose"),
1310
1309
  on the model `keryx review tier --fix-attempt <N> --findings <n> --json` computes
1311
- 4. Run the sanity check (step 2.5.2 logic) — verify commits were made
1310
+ 4. Run the sanity check (step 2.5.2 logic) — verify commits were made — and `contracts validate --resolved` on the result
1312
1311
  5. Run the next managed round — the FULL 2.6.1 sequence, not a bare re-dispatch:
1313
1312
  keryx review budget --spent <usd> --outstanding <n>
1314
1313
  keryx review plan --target branch --ref <feature-branch> --head <new-head>
@@ -129,10 +129,18 @@ All subsequent operations ONLY in the worktree directory.
129
129
  WAVES = dependency_order from ANALYSIS_RESULT (already topologically sorted)
130
130
 
131
131
  FOR wave_index, wave_tasks in enumerate(WAVES):
132
- 1. Dispatch one tests-creator per task, in a SINGLE turn (parallel). Wait for all.
132
+ 1. keryx skills resolve-context --for test --files <target_files> --task-type <type> --task-text '<task title and description, each ' written as '\''>' --out <JOBS_ROOT>/<job-name>/results/<task_id>.test.resolved.json
133
+ Dispatch one tests-creator per task, in a SINGLE turn (parallel), each carrying
134
+ resolved_context_file. Wait for all; per result run
135
+ keryx skills contracts validate <result.json> --schema tests-creator-output --resolved <that file>
133
136
  Load skill: skills/gdskills/quality/tests-creator/SKILL.md
134
137
  2. Parallel safety check: tasks sharing a target_file run sequentially.
135
- 3. Dispatch one task-implementer per task, in a SINGLE turn (parallel). Wait for all.
138
+ 3. keryx skills resolve-context --for implement ... --out <...>/<task_id>.implement.resolved.json
139
+ A promoted entry (reason condition:glob:<g> or condition:keyword:<k>) needs path, sha256, the declared anchor, a summary
140
+ (40+ characters, own words) and how read|followed in the worker's project_context_applied; validate refuses it otherwise.
141
+ Dispatch one task-implementer per task, in a SINGLE turn (parallel), each carrying
142
+ resolved_context_file. Wait for all; per result run
143
+ keryx skills contracts validate <result.json> --schema task-implementer-output --resolved <that file>
136
144
  Load skill: skills/gdskills/orchestration/task-implementer/SKILL.md
137
145
  4. Read each STATUS line:
138
146
  all DONE → next wave
@@ -177,8 +185,8 @@ ingest carrying a scope-B finding without it is refused. Severities are
177
185
 
178
186
  If `blocker` or `major` findings exist:
179
187
  1. Group by file
180
- 2. Launch task-implementer (`task_type: "fix"`) with the findings
181
- 3. Run the sanity check — verify commits were made
188
+ 2. Re-run `keryx skills resolve-context --for implement` on the fix files (with `--task-text '<fix task description, each ' written as '\''>'`), then launch task-implementer (`task_type: "fix"`) with the findings and `resolved_context_file`
189
+ 3. Run the sanity check — verify commits were made — and `contracts validate --resolved` on the result
182
190
  4. Run the NEXT managed round: `keryx review plan --target branch --ref <feature-branch> --head <new-head>` (or `review start`) on the new head, then review-orchestrator. The round, range and prior findings come from `round.json`: no own range, no `--all` per round, no whole-branch re-review.
183
191
  5. Disposition every finding of the previous round:
184
192
  ```bash
@@ -173,35 +173,34 @@ Before you rely on a project-skill's guidance, confirm it still matches the code
173
173
  - `keryx skills route <target_file>` — find the project-skill for this module/entity (if any).
174
174
  - If one exists: `keryx skills verify <module>/<skill>` — classifies it `fresh | stale | needs-review | blocked`.
175
175
  - If it is **not `fresh`**: do not follow it blindly. Verify each claim against the code you read in Phase 2, and note the drift in `notes` (Phase 6.1) so the orchestrator can trigger `skills learn`.
176
+ - If it is **`fresh`**: apply it. Its rules, templates and conventions for the target are part of the task, not background reading, and they supplement this skill's phases: a project skill never replaces a phase, the TDD order or the output contract. Where it and a bundled rule disagree, the project skill wins; say so in `notes`.
176
177
  - If no skill exists for a non-trivial module you had to reverse-engineer, note that too — it's a candidate for `skills create`.
177
178
 
178
179
  This step is read-only and inline; do not spawn a subagent for it.
179
180
 
181
+ **2.0c Resolve the project's standard (before 2.1):**
182
+ - Use `workspace.resolved_context_file` when given; otherwise run `keryx skills resolve-context --for implement --files <target_files, comma-joined> --task-type <task_type> --task-text '<task.description, each ' written as '\''>' --out <project-relative path>` (`--task-text` lets a declared keyword promote a document).
183
+ - Read every `must_read` entry in full, project entries first; read `on_demand` ones only when the work touches them. A project rule listed with `shadows` replaces the bundled one. An entry with `also_at` is one file under several paths: citing any of them, with the entry's sha256, counts.
184
+ - An entry with reason `condition:glob:<g>` or `condition:keyword:<k>` was promoted by a declared condition and carries an `anchor`, a `description` and `section_bytes`: read the anchored section in full. Its `project_context_applied` item needs the `path`, the `sha256` of the whole file as pinned, the `anchor` exactly as declared, a `summary` and `how` `read` or `followed`, never `n/a`. Write the `summary` in your own words, at least 40 characters, naming at least three distinct words of five or more letters taken from the anchored section; `contracts validate --resolved` rejects a copied hash without it. Cite what you used in `project_context_applied` (6.1) with the resolver's `sha256`; `n/a` needs a reason in `notes`. Never edit a resolved file: `keryx skills contracts validate <result.json> --schema task-implementer-output --resolved <resolved.json>` fails on drift.
185
+
180
186
  **2.1 Read all target files:**
181
187
  - Read each file from `target_files` in full
182
188
  - If a file does not exist yet, note it as "new file to create"
183
189
  - Read the `context` field for additional type/signature info
184
190
 
185
191
  **2.2 Read existing tests and stories:**
186
- - `existing_tests` and `existing_stories` are arrays and default to `[]`
187
- (`input-contract.schema.json`). Empty means none; the string `"none"` is not a
188
- legal value and a request carrying it is refused by 1.4
192
+ - `existing_tests` and `existing_stories` default to `[]`; the string `"none"` is not a legal value and 1.4 refuses it
189
193
  - Read each file listed in either array
190
194
  - Understand existing test patterns (describe/it structure, mocks, fixtures) and, for every file in `existing_stories`, the story patterns it uses (`Meta`/`StoryObj` shape, how variants are declared, how callbacks are stubbed) — 4.4 builds on what you record here
191
195
 
192
196
  **2.3 Read module neighbors:**
193
197
  - List sibling files in the same directory as each target file
194
198
  - Read 2-3 similar files to understand module patterns (naming, exports, structure)
195
- - Pay attention to:
196
- - Import aliases used (e.g., `@components`, `@utils`)
197
- - Export patterns (named vs default)
198
- - TypeScript patterns (interfaces vs types, generics usage)
199
- - Component patterns (observer wrapping, props interface naming)
200
- - Store patterns (makeObservable(this), explicit decorators, private fields before public fields, thin public @action.bound UI actions, non-mutating public helpers, private API methods, runInAction inside private mutation blocks after await)
199
+ - Pay attention to import aliases, named vs default exports, interfaces vs types, observer wrapping and props naming, and store patterns (makeObservable(this), explicit decorators, private fields before public, thin public @action.bound UI actions, non-mutating public helpers, private API methods, runInAction after await)
201
200
 
202
201
  **2.4 Load relevant rules (from `module_patterns` or by detection):**
203
202
 
204
- Based on what you're implementing, load and follow the relevant project rules.
203
+ Rules `resolve-context` listed are already read (2.0c); load the rest by what you're implementing.
205
204
 
206
205
  **Always load (all task types):**
207
206
  - `tdd-workflow.mdc` — red-green-refactor, STATUS: DONE requires passing tests
@@ -224,8 +223,7 @@ Based on what you're implementing, load and follow the relevant project rules.
224
223
  - `async`/`await` or queue code → `async-patterns.mdc`
225
224
  - New architectural layers or modules → `clean-architecture.mdc`
226
225
 
227
- Rules live at `.metaproject/rules/core/<rule>.mdc` on every harness — that is the
228
- one tree `keryx init` installs and the one every build of this skill reads.
226
+ Resolve every rule above project first, bundled second: read `.metaproject/rules/project/<rule>.mdc` when it exists, otherwise `.metaproject/rules/core/<rule>.mdc`. A project rule with the same name replaces the bundled one (never read both as if they agreed). `.metaproject/rules/core/` is the tree `keryx init` installs and `keryx update` overwrites, so a project's own rules live only in `rules/project/`.
229
227
 
230
228
  ### Phase 3: PLAN
231
229
 
@@ -427,6 +425,7 @@ Write full JSON to `<JOBS_ROOT>/<JOB_NAME>/results/<task_id>.json`:
427
425
  "story_result": "<pass|build error: details|not applicable>",
428
426
  "acceptance_criteria_met": "<all|partial: list of unmet criteria|none>",
429
427
  "skill_drift": "<none | stale: <module>/<skill> — <what diverged> | missing: <module> should have a project-skill>",
428
+ "project_context_applied": [{ "path": "<as the resolver printed it>", "sha256": "<resolver's sha256>", "how": "read|followed|n/a", "anchor": "<declared heading; promoted entries only>", "summary": "<own words, 40+ chars; promoted entries only>" }],
430
429
  "noticed_not_touched": [{ "what": "<problem>", "where": "<path|symbol>" }],
431
430
  "assumptions": ["<what you assumed where the task did not say>"],
432
431
  "behavior_changes": [{ "description": "<what now behaves differently>", "user_visible": true, "needs_operator": true, "alternatives": ["<option you did not take>"] }],
@@ -134,6 +134,11 @@
134
134
  "context_path": {
135
135
  "type": "string",
136
136
  "description": "Job context document read at the start of Phase 2, e.g. .metaproject/jobs/<job_name>/ai/context.md"
137
+ },
138
+ "resolved_context_file": {
139
+ "type": "string",
140
+ "description": "Output of `keryx skills resolve-context --for implement --out <path>` for this task, made by the dispatcher. Present => Phase 2 reads its `must_read` entries instead of resolving again, and the result must carry `project_context_applied`. Absent => Phase 2 runs the resolver itself.",
141
+ "examples": [".metaproject/data/flows/461/task-1.resolved.json"]
137
142
  }
138
143
  }
139
144
  },
@@ -97,6 +97,22 @@
97
97
  }
98
98
  }
99
99
  },
100
+ "project_context_applied": {
101
+ "type": "array",
102
+ "description": "Which of the files `keryx skills resolve-context` listed this run actually used, so a reader can subtract it from the resolver's `must_read` list. `how`: `read` - the file was opened and its text used; `followed` - its rules were applied through a skill or another file that already contains them; `n/a` - the file does not touch the task, with the reason in `notes`. `sha256` is the resolver's value for the file, not a fresh hash: `keryx skills contracts validate --resolved` compares the two.",
103
+ "items": {
104
+ "type": "object",
105
+ "additionalProperties": false,
106
+ "required": ["path", "sha256", "how"],
107
+ "properties": {
108
+ "path": { "type": "string", "minLength": 1, "description": "Repo-relative path exactly as the resolver printed it" },
109
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "The resolver's sha256 of the file" },
110
+ "how": { "type": "string", "enum": ["read", "followed", "n/a"] },
111
+ "anchor": { "type": "string", "description": "Optional. The heading of the section read, exactly as the resolver printed it for an entry promoted by a declared condition (flow 462)" },
112
+ "summary": { "type": "string", "description": "Optional. What the section says, in the worker's own words; for a promoted entry `keryx skills contracts validate --resolved` checks it against the anchored section (flow 462)" }
113
+ }
114
+ }
115
+ },
100
116
  "notes": {
101
117
  "type": "string",
102
118
  "description": "Warnings, blockers, or additional context. The three fields above were carved out of this one: free text is where they went before they were declared, and anything that fits one of them belongs there instead."
@@ -90,9 +90,10 @@ not a hardcoded `cat`/`ls`/`find` invocation).
90
90
  - Fixture/factory patterns (how test data is created)
91
91
  - Before/after hook usage
92
92
 
93
+ **1.2b Load test-convention rules, project first.** Read the testing rules the task touches (`testing-conventions`, `tdd-workflow`, and any rule the task names) in this order: `.metaproject/rules/project/<rule>.mdc` first, then `.metaproject/rules/core/<rule>.mdc`. A project rule with the same name replaces the bundled one; a project rule with a different name adds to it. Take the list from `workspace.resolved_context_file` when the task carries one, otherwise run `keryx skills resolve-context --for test --files <target_files, comma-joined> --task-text '<task description, each ' written as '\''>' --out <project-relative path>` (`--task-text` lets a declared keyword promote a document); read its `must_read` entries in full and record each in `project_context_applied` with the resolver's `sha256`. An entry with reason `condition:glob:<g>` or `condition:keyword:<k>` was promoted by a declared condition: read its anchored section, and its item needs the `path`, the `sha256` of the whole file as pinned, the `anchor` exactly as declared, a `summary` (at least 40 characters, in your own words, naming at least three distinct words of five or more letters from that section) and `how` `read` or `followed`, never `n/a`. Where a project test convention and a bundled one conflict, the project wins. A project skill only supplements these steps; it never replaces this skill.
94
+
93
95
  **1.3 Identify test file naming:**
94
- - Pattern: `<component>.test.ts`, `<service>.spec.ts`, `test_<module>.py`, etc.
95
- - Location: co-located with source or in `__tests__`/`tests/` directory
96
+ - Pattern (`<component>.test.ts`, `<service>.spec.ts`, `test_<module>.py`) and location (co-located, or `__tests__`/`tests/`)
96
97
 
97
98
  **Output of Phase 1:**
98
99
  ```
@@ -200,7 +201,6 @@ The generated tests MUST:
200
201
  ```typescript
201
202
  // Use todo() for unimplemented tests
202
203
  it.todo('<test description>');
203
-
204
204
  // OR use a stub assertion that fails
205
205
  it('<test description>', () => {
206
206
  expect(true).toBe(false); // RED: remove this when implementing
@@ -265,6 +265,8 @@ TEST_CASE_SPECS:
265
265
  type: happy_path | edge_case | error_path
266
266
  status: written # test file exists, test is RED
267
267
 
268
+ project_context_applied: [{path: <rule path, project first>, sha256: <resolver's sha256>, how: read|followed|n/a, anchor: <declared heading, promoted entries only>, summary: <own words, 40+ chars, promoted entries only>}]
269
+
268
270
  run_command: "<command to run just these tests>"
269
271
  expected_result: "all failing (RED phase)"
270
272
  notes: "<any test design decisions or assumptions>"
@@ -285,12 +287,10 @@ all_criteria_covered: true | false (with explanation)
285
287
 
286
288
  When `task-implementer` receives a task that includes `test_case_specs`:
287
289
 
288
- 1. Read the test files (already committed as RED)
289
- 2. Run the tests — confirm they FAIL
290
- 3. Implement code until all tests are GREEN
291
- 4. Commit the implementation
292
- 5. Re-run tests — confirm GREEN
293
- 6. Report SUCCESS
290
+ 1. Read the test files (already committed as RED) and run them — confirm they FAIL
291
+ 2. Implement code until all tests are GREEN
292
+ 3. Commit the implementation, re-run tests — confirm GREEN
293
+ 4. Report SUCCESS
294
294
 
295
295
  This ensures the TDD cycle is maintained end-to-end.
296
296
 
@@ -75,6 +75,10 @@
75
75
  "type": "integer",
76
76
  "minimum": 1,
77
77
  "description": "GitHub issue number for commit references"
78
+ },
79
+ "resolved_context_file": {
80
+ "type": "string",
81
+ "description": "Output of `keryx skills resolve-context --for test --out <path>` for this task, made by the dispatcher. Present => Phase 1 reads its `must_read` entries instead of resolving again, and the result must carry `project_context_applied`. Absent => Phase 1 runs the resolver itself."
78
82
  }
79
83
  }
80
84
  },
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "tests-creator-output-contract",
4
4
  "title": "Tests Creator Output Contract",
5
- "description": "Output from tests-creator: test case specifications for task-implementer plus status report.",
5
+ "description": "Output from tests-creator: test case specifications for task-implementer plus status report. Registered as contract `tests-creator-output`: validate a result with `keryx skills contracts validate <result.json> --schema tests-creator-output`.",
6
6
  "type": "object",
7
7
  "required": ["test_case_specs", "status_report"],
8
8
  "properties": {
@@ -85,6 +85,22 @@
85
85
  }
86
86
  }
87
87
  },
88
+ "project_context_applied": {
89
+ "type": "array",
90
+ "description": "Which of the files `keryx skills resolve-context` listed this run actually used, so a reader can subtract it from the resolver's `must_read` list. `how`: `read` - the file was opened and its text used; `followed` - its rules were applied through a skill or another file that already contains them; `n/a` - the file does not touch the task, with the reason in `notes`. `sha256` is the resolver's value for the file, not a fresh hash: `keryx skills contracts validate --resolved` compares the two.",
91
+ "items": {
92
+ "type": "object",
93
+ "additionalProperties": false,
94
+ "required": ["path", "sha256", "how"],
95
+ "properties": {
96
+ "path": { "type": "string", "minLength": 1, "description": "Repo-relative path exactly as the resolver printed it" },
97
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "The resolver's sha256 of the file" },
98
+ "how": { "type": "string", "enum": ["read", "followed", "n/a"] },
99
+ "anchor": { "type": "string", "description": "Optional. The heading of the section read, exactly as the resolver printed it for an entry promoted by a declared condition (flow 462)" },
100
+ "summary": { "type": "string", "description": "Optional. What the section says, in the worker's own words; for a promoted entry `keryx skills contracts validate --resolved` checks it against the anchored section (flow 462)" }
101
+ }
102
+ }
103
+ },
88
104
  "status_report": {
89
105
  "type": "object",
90
106
  "required": ["status", "tests_written", "all_criteria_covered"],