@mrciphersmith/keryx 0.3.112 → 0.3.113

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 (37) hide show
  1. package/README.md +17 -3
  2. package/THIRD_PARTY_NOTICES.md +56 -0
  3. package/dist/cli.js +106311 -101534
  4. package/dist/core.js +5525 -4329
  5. package/package.json +3 -1
  6. package/src/gdgraph/affected-report.ts +14 -4
  7. package/src/gdgraph/build.ts +12 -9
  8. package/src/gdgraph/core-sources.ts +1 -0
  9. package/src/gdgraph/data-dir.ts +14 -0
  10. package/src/gdgraph/enrich.ts +5 -3
  11. package/src/gdgraph/query.ts +3 -2
  12. package/src/gdgraph/staleness.ts +8 -1
  13. package/src/gdgraph/wiki-layer.ts +9 -6
  14. package/src/gdskills/bundled/skills/orchestration/flow-orchestrator/SKILL.md +3 -3
  15. package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.md +3 -3
  16. package/src/gdskills/bundled/skills/orchestration/job-orchestrator/orchestrator-prompt.md +2 -2
  17. package/src/gdskills/bundled/skills/orchestration/task-implementer/SKILL.md +4 -4
  18. package/src/gdskills/bundled/skills/planning/autodoc-orchestrator/SKILL.md +1 -1
  19. package/src/gdskills/bundled/skills/planning/interviewer/SKILL.md +3 -3
  20. package/src/gdskills/bundled/skills/quality/api-truth/SKILL.md +2 -2
  21. package/src/gdskills/bundled/skills/quality/deprecation-path/SKILL.md +3 -3
  22. package/src/gdskills/bundled/skills/quality/fresh-eyes/SKILL.md +2 -2
  23. package/src/gdskills/bundled/skills/quality/perf-check/SKILL.md +1 -1
  24. package/src/gdskills/bundled/skills/quality/root-cause/SKILL.md +2 -2
  25. package/src/gdskills/bundled/skills/review/review-architecture/SKILL.md +14 -15
  26. package/src/gdskills/bundled/skills/review/review-backend/SKILL.md +14 -14
  27. package/src/gdskills/bundled/skills/review/review-frontend/SKILL.md +13 -16
  28. package/src/gdskills/bundled/skills/review/review-logic/SKILL.md +14 -14
  29. package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +3 -3
  30. package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +19 -17
  31. package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-finding.schema.json +16 -0
  32. package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-input.schema.json +40 -0
  33. package/src/gdskills/bundled/skills/review/review-orchestrator/templates/pr-comment-frontend.md +1 -1
  34. package/src/gdskills/bundled/skills/review/review-performance/SKILL.md +13 -13
  35. package/src/gdskills/bundled/skills/review/review-regression/SKILL.md +23 -25
  36. package/src/gdskills/bundled/skills/review/review-security-code/SKILL.md +13 -15
  37. package/src/gdskills/bundled/skills/review/review-testing-practices/SKILL.md +31 -57
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.112",
3
+ "version": "0.3.113",
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": {
@@ -53,6 +53,7 @@
53
53
  "test:client": "bun run test:client:terminal && bun run test:client:streaming && bun run test:client:cancel-resume && bun run test:client:runtime",
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
+ "docs:build": "mkdocs build --strict",
56
57
  "check:doc-links": "bun scripts/check-doc-links.ts",
57
58
  "check:retired-spellings": "bun scripts/check-retired-cli-spellings.ts",
58
59
  "baseline:review-precision": "bun scripts/review-precision-baseline.ts",
@@ -67,6 +68,7 @@
67
68
  "src/gdskills/contracts",
68
69
  "!src/**/*.test.ts",
69
70
  "LICENSE",
71
+ "THIRD_PARTY_NOTICES.md",
70
72
  "README.md",
71
73
  "package.json"
72
74
  ],
@@ -25,6 +25,10 @@ import { explainAbsentGraphTarget, loadDeletionTrail } from "../forgetting/servi
25
25
  export interface AffectedReportOptions {
26
26
  depth?: number;
27
27
  ranked?: boolean;
28
+ /** Read this gdgraph data directory (a review package's graph) instead of the project's own. */
29
+ dataDir?: string;
30
+ /** Freshness to report for a `dataDir` graph; the project graph's own staleness check says nothing about it. */
31
+ freshness?: StalenessCheck;
28
32
  }
29
33
 
30
34
  export interface AffectedReport {
@@ -58,7 +62,13 @@ export async function buildAffectedReport(
58
62
  const depth = opts.depth !== undefined && Number.isFinite(opts.depth) ? opts.depth : config.affected.defaultDepth;
59
63
  const ranked = opts.ranked ?? true;
60
64
 
61
- const graph = await loadGraph(root);
65
+ const graph = await loadGraph(root, opts.dataDir);
66
+ const freshnessOf = (): Promise<StalenessCheck> =>
67
+ opts.dataDir === undefined
68
+ ? checkGraphStaleness(root)
69
+ : Promise.resolve(
70
+ opts.freshness ?? { status: "unknown", reasons: ["package graph: freshness is recorded in layers.json"] },
71
+ );
62
72
 
63
73
  // Symbol-aware: if the target isn't a known file but names a symbol,
64
74
  // resolve it to its owning file — mirrors `runAffected`'s own resolution.
@@ -83,7 +93,7 @@ export async function buildAffectedReport(
83
93
  "the graph index holds no file nodes — it was never built here, or its storage is unreadable. " +
84
94
  "No claim is being made about whether this target exists.",
85
95
  );
86
- const freshness = await checkGraphStaleness(root);
96
+ const freshness = await freshnessOf();
87
97
  return {
88
98
  exitCode: 1,
89
99
  resolutionNote,
@@ -108,7 +118,7 @@ export async function buildAffectedReport(
108
118
  `gdgraph: "${target}" is not a node in the built graph (never indexed, or the ` +
109
119
  `path/symbol does not exist) — this is not the same as an indexed target with ` +
110
120
  `zero edges. Run \`keryx gdgraph build\` if the file is new, or double-check the path.`;
111
- const freshness = await checkGraphStaleness(root);
121
+ const freshness = await freshnessOf();
112
122
  return {
113
123
  exitCode: 1,
114
124
  resolutionNote,
@@ -132,7 +142,7 @@ export async function buildAffectedReport(
132
142
  };
133
143
  }
134
144
 
135
- const freshness: StalenessCheck = await checkGraphStaleness(root);
145
+ const freshness: StalenessCheck = await freshnessOf();
136
146
  return {
137
147
  exitCode: 0,
138
148
  resolutionNote,
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { existsSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import { gdgraphArtifactsDir, gdgraphStorageDir } from "./data-dir";
4
5
  import type { GraphData, GraphEdge, GraphNode, ImportKind, TranspilerImportKind } from "./types";
5
6
  import { TYPE_ONLY_IMPORT_KIND, UNKNOWN_IMPORT_KIND } from "./types";
6
7
 
@@ -100,7 +101,8 @@ type ResolverSet = {
100
101
  python: ImportResolver;
101
102
  };
102
103
 
103
- export async function buildGraph(projectRoot: string): Promise<BuildResult> {
104
+ export async function buildGraph(projectRoot: string, options: { dataDir?: string } = {}): Promise<BuildResult> {
105
+ const { dataDir } = options;
104
106
  const collection = await collectSourceFiles(projectRoot);
105
107
  const files = collection.files;
106
108
  const fileSet = new Set(files);
@@ -157,8 +159,8 @@ export async function buildGraph(projectRoot: string): Promise<BuildResult> {
157
159
  }
158
160
 
159
161
  const graph = { nodes: [...nodes, ...assetNodes.values()].sort((a, b) => a.path.localeCompare(b.path)), edges };
160
- await writeGraph(projectRoot, graph);
161
- const summaryPath = await writeSummary(projectRoot, graph, collection);
162
+ await writeGraph(projectRoot, graph, dataDir);
163
+ const summaryPath = await writeSummary(projectRoot, graph, collection, dataDir);
162
164
 
163
165
  // B1: after the UNCHANGED file-level build, optionally enrich with the
164
166
  // tree-sitter symbol layer behind the Block 0 capability seam. Loaded
@@ -168,7 +170,7 @@ export async function buildGraph(projectRoot: string): Promise<BuildResult> {
168
170
  // artifacts stay byte-identical (the golden rule, B-1/C0-7/F-3).
169
171
  try {
170
172
  const { enrichBuildWithSymbols } = await import("./enrich");
171
- await enrichBuildWithSymbols(projectRoot, fileRecords);
173
+ await enrichBuildWithSymbols(projectRoot, fileRecords, undefined, dataDir);
172
174
  } catch {
173
175
  // Enrichment module or seam unavailable ⇒ file-level graph only.
174
176
  }
@@ -180,7 +182,7 @@ export async function buildGraph(projectRoot: string): Promise<BuildResult> {
180
182
  // `edges.jsonl` written above are never revisited (AC13).
181
183
  try {
182
184
  const { enrichBuildWithWikiLayer } = await import("./wiki-layer");
183
- await enrichBuildWithWikiLayer({ projectRoot, graph, fileRecords });
185
+ await enrichBuildWithWikiLayer({ projectRoot, graph, fileRecords, ...(dataDir === undefined ? {} : { dataDir }) });
184
186
  } catch {
185
187
  // Wiki absent, unreadable, or the layer module unavailable ⇒ no layer.
186
188
  }
@@ -887,9 +889,9 @@ function isRecord(value: unknown): value is Record<string, unknown> {
887
889
  return typeof value === "object" && value !== null && !Array.isArray(value);
888
890
  }
889
891
 
890
- async function writeGraph(projectRoot: string, graph: GraphData): Promise<void> {
891
- const storageDir = path.join(projectRoot, ".metaproject", "data", "gdgraph", "storage");
892
- const artifactsDir = path.join(projectRoot, ".metaproject", "data", "gdgraph", "artifacts");
892
+ async function writeGraph(projectRoot: string, graph: GraphData, dataDir?: string): Promise<void> {
893
+ const storageDir = gdgraphStorageDir(projectRoot, dataDir);
894
+ const artifactsDir = gdgraphArtifactsDir(projectRoot, dataDir);
893
895
  await mkdir(storageDir, { recursive: true });
894
896
  await mkdir(artifactsDir, { recursive: true });
895
897
 
@@ -914,8 +916,9 @@ async function writeSummary(
914
916
  projectRoot: string,
915
917
  graph: GraphData,
916
918
  collection: SourceCollection,
919
+ dataDir?: string,
917
920
  ): Promise<string> {
918
- const artifactsDir = path.join(projectRoot, ".metaproject", "data", "gdgraph", "artifacts");
921
+ const artifactsDir = gdgraphArtifactsDir(projectRoot, dataDir);
919
922
  await mkdir(artifactsDir, { recursive: true });
920
923
 
921
924
  const unresolved = graph.edges.filter((edge) => edge.kind === "unresolved");
@@ -14,6 +14,7 @@
14
14
  // instead of a stranger's first five minutes.
15
15
  export const GDGRAPH_CORE_SOURCES: readonly string[] = [
16
16
  "build.ts",
17
+ "data-dir.ts",
17
18
  "query.ts",
18
19
  "target.ts",
19
20
  "types.ts",
@@ -0,0 +1,14 @@
1
+ import path from "node:path";
2
+
3
+ /** The gdgraph data directory (holds `storage/` and `artifacts/`): the project's own unless a caller names another. */
4
+ export function gdgraphDataDir(projectRoot: string, dataDir?: string): string {
5
+ return dataDir ?? path.join(projectRoot, ".metaproject", "data", "gdgraph");
6
+ }
7
+
8
+ export function gdgraphStorageDir(projectRoot: string, dataDir?: string): string {
9
+ return path.join(gdgraphDataDir(projectRoot, dataDir), "storage");
10
+ }
11
+
12
+ export function gdgraphArtifactsDir(projectRoot: string, dataDir?: string): string {
13
+ return path.join(gdgraphDataDir(projectRoot, dataDir), "artifacts");
14
+ }
@@ -17,6 +17,7 @@ import {
17
17
  type CapabilitySpec,
18
18
  } from "../capability/seam";
19
19
  import { loadGdgraphConfig } from "./config";
20
+ import { gdgraphStorageDir } from "./data-dir";
20
21
  import {
21
22
  createTreesitterSpec,
22
23
  resolveTreesitterCapability,
@@ -45,6 +46,7 @@ export async function enrichBuildWithSymbols(
45
46
  // `resolveTreesitterCapability`; tests pass an explicit `CapabilityResolver`
46
47
  // (e.g. a mock adapter, or the raw seam) to bypass it.
47
48
  resolve?: CapabilityResolver,
49
+ dataDir?: string,
48
50
  ): Promise<EnrichResult> {
49
51
  const config = await loadGdgraphConfig(cwd);
50
52
  const treesitterConfig = {
@@ -76,12 +78,12 @@ export async function enrichBuildWithSymbols(
76
78
  () => ({ symbols: [], calls: [] }),
77
79
  );
78
80
 
79
- await writeSymbolLayer(cwd, layer);
81
+ await writeSymbolLayer(cwd, layer, dataDir);
80
82
  return { enriched: true, symbols: layer.symbols.length, calls: layer.calls.length };
81
83
  }
82
84
 
83
- async function writeSymbolLayer(cwd: string, layer: SymbolLayer): Promise<void> {
84
- const storageDir = path.join(cwd, ".metaproject", "data", "gdgraph", "storage");
85
+ async function writeSymbolLayer(cwd: string, layer: SymbolLayer, dataDir?: string): Promise<void> {
86
+ const storageDir = gdgraphStorageDir(cwd, dataDir);
85
87
  await mkdir(storageDir, { recursive: true });
86
88
  await writeFile(
87
89
  path.join(storageDir, "symbols.jsonl"),
@@ -10,10 +10,11 @@ import type {
10
10
  WikiPageNode,
11
11
  } from "./types";
12
12
  import { TYPE_ONLY_IMPORT_KIND } from "./types";
13
+ import { gdgraphStorageDir } from "./data-dir";
13
14
  import { resolveGraphTarget } from "./target";
14
15
 
15
- export async function loadGraph(projectRoot: string): Promise<GraphData> {
16
- const storageDir = path.join(projectRoot, ".metaproject", "data", "gdgraph", "storage");
16
+ export async function loadGraph(projectRoot: string, dataDir?: string): Promise<GraphData> {
17
+ const storageDir = gdgraphStorageDir(projectRoot, dataDir);
17
18
  const nodes = await readJsonl<GraphNode>(path.join(storageDir, "nodes.jsonl"));
18
19
  const edges = await readJsonl<GraphEdge>(path.join(storageDir, "edges.jsonl"));
19
20
  // B1 symbol layer: loaded ONLY if present. Missing files ⇒ empty/omitted layer
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { revisionOperand } from "../lib/git-revision";
4
5
  import { gitCmd, gitHead, readProvenance } from "../sync/provenance";
5
6
 
6
7
  // W7 AC4 follow-up (flow 304 T8) — a real, previously-latent defect found
@@ -237,7 +238,13 @@ async function committedChangedFiles(
237
238
  // "HEAD moved" reason. `--name-only -z` lists only the destination path per
238
239
  // entry even for a rename (unlike `git status -z`, which pairs it with an
239
240
  // orig-path field), so this stays a plain NUL-split with no pairing logic.
240
- const diff = await gitCmd(cwd, ["diff", "--name-only", "-z", fromCommit, toCommit]);
241
+ let diff: string | null;
242
+ try {
243
+ diff = await gitCmd(cwd, ["diff", "--name-only", "-z", ...revisionOperand(fromCommit), toCommit]);
244
+ } catch {
245
+ // A provenance commit that reads as an option is refused by revisionOperand; this check never throws.
246
+ return [];
247
+ }
241
248
  if (diff === null) {
242
249
  return [];
243
250
  }
@@ -15,6 +15,7 @@
15
15
  import { createHash } from "node:crypto";
16
16
  import { mkdir, stat, writeFile } from "node:fs/promises";
17
17
  import path from "node:path";
18
+ import { gdgraphStorageDir } from "./data-dir";
18
19
  import { collectPages } from "../wiki/collect";
19
20
  import { computeModuleKeyFiles } from "../wiki/collect";
20
21
  import { resolveDescribeSet } from "../wiki/describes";
@@ -25,8 +26,8 @@ export const WIKI_PAGES_FILE = "wiki-pages.jsonl";
25
26
  export const DESCRIBES_FILE = "describes.jsonl";
26
27
  export const BUILD_MANIFEST_FILE = "build-manifest.json";
27
28
 
28
- function storageDir(projectRoot: string): string {
29
- return path.join(projectRoot, ".metaproject", "data", "gdgraph", "storage");
29
+ function storageDir(projectRoot: string, dataDir?: string): string {
30
+ return gdgraphStorageDir(projectRoot, dataDir);
30
31
  }
31
32
 
32
33
  /**
@@ -181,8 +182,10 @@ export async function enrichBuildWithWikiLayer(input: {
181
182
  /** Injected in tests; defaults to the real `validModuleNames`. */
182
183
  validModules?: Set<string> | undefined;
183
184
  loadValidModules?: (projectRoot: string) => Promise<Set<string> | undefined>;
185
+ /** A gdgraph data directory other than the project's own (a review package's graph). */
186
+ dataDir?: string;
184
187
  }): Promise<WikiLayer> {
185
- const dir = storageDir(input.projectRoot);
188
+ const dir = storageDir(input.projectRoot, input.dataDir);
186
189
  await mkdir(dir, { recursive: true });
187
190
 
188
191
  const mtimes = new Map<string, number>();
@@ -205,7 +208,7 @@ export async function enrichBuildWithWikiLayer(input: {
205
208
  const validModules =
206
209
  input.validModules !== undefined
207
210
  ? input.validModules
208
- : await (input.loadValidModules ?? defaultLoadValidModules)(input.projectRoot);
211
+ : await (input.loadValidModules ?? ((root) => defaultLoadValidModules(root, input.dataDir)))(input.projectRoot);
209
212
 
210
213
  const wikiPages = await collectPages(input.projectRoot);
211
214
  const contents = new Map<string, string>();
@@ -236,9 +239,9 @@ export async function enrichBuildWithWikiLayer(input: {
236
239
  return layer;
237
240
  }
238
241
 
239
- async function defaultLoadValidModules(projectRoot: string): Promise<Set<string> | undefined> {
242
+ async function defaultLoadValidModules(projectRoot: string, dataDir?: string): Promise<Set<string> | undefined> {
240
243
  // Dynamic so `gdgraph` keeps no static dependency on `wiki/service`, which
241
244
  // reads the graph itself — the same cycle-avoidance the symbol layer uses.
242
245
  const { validModuleNames } = await import("../wiki/service");
243
- return validModuleNames(projectRoot);
246
+ return validModuleNames(projectRoot, dataDir);
244
247
  }
@@ -23,7 +23,7 @@ license: "MIT"
23
23
 
24
24
  ## Preflight
25
25
 
26
- Run in the main session: this skill dispatches workers. Without the `Agent` tool, or with the depth budget spent (Claude Code nests at most 3 deep), the whole output is this line, with no tool calls and no work of your own: `STATUS: BLOCKED nested_dispatch_unavailable — run this skill in the main session`
26
+ Run in the main session: this skill dispatches workers. Without a subagent-spawn tool (`Agent`, `spawn_subagent` or the host's equivalent), or with the depth budget spent (Claude Code nests at most 3 deep), the whole output is this line, with no tool calls and no work of your own: `STATUS: BLOCKED nested_dispatch_unavailable — run this skill in the main session`
27
27
 
28
28
  ## Purpose
29
29
 
@@ -560,7 +560,7 @@ the bound plus an escalation — never an unbounded loop.
560
560
  **0.820 -> 0.673** across two forced revisions while cumulative ever-correct
561
561
  is **0.847** ([arXiv:2607.24604](https://arxiv.org/abs/2607.24604)) — the
562
562
  agent finds the fix and then destroys it, throwing away ~15 points by not
563
- stopping. Aider hardcodes `max_reflections = 3`; OpenHands' critic uses 3.
563
+ stopping. Other agent tools hardcode the same bound (`max_reflections = 3`).
564
564
  Rounds four through six of a self-fix loop were not buying convergence; they
565
565
  were buying regressions. Review rounds are the exception, as stated above.
566
566
 
@@ -690,5 +690,5 @@ Stop and re-read this skill if you are thinking:
690
690
  | "Tests pass and the review is clean, so I'll open the PR and complete the flow." | Phase 4 stops and asks the user how the flow should end; not every flow wants a PR. And completion requires a confirmed merge into the base branch captured at creation — not a green local run. |
691
691
  | "The worker returned BLOCKED twice — faster if I implement this task myself." | The implementer never self-accepts and the orchestrator never implements. Block the flow, escalate one concise question, then unblock and re-dispatch. Doing the work here erases the boundary the whole flow model rests on. |
692
692
  | "Verification is described in the plan, so it will happen." | A verification step in the plan is a task, not a sentence. If it is not a task with a status, nothing records whether it ran, and the flow reaches `implemented` with an unrun gate. |
693
- | "The review fan-out is cheap, so the budget check can wait." | `keryx review budget --spent … --outstanding …` gates the fan-out, and `review-orchestrator` nests under this skill where keryx cannot see the in-flight subagents. Skipping the check means the cap bounds nothing. |
693
+ | "The review fan-out is cheap, so the budget check can wait." | `keryx review budget --spent … --outstanding …` gates the fan-out, and `review-orchestrator` runs inline in this same main session, where keryx cannot see the in-flight subagents; declare them with `--outstanding`. Skipping the check means the cap bounds nothing. |
694
694
  | "I'll set the verification kind later." | An `unclassified` criterion cannot become data afterwards without a recorded rewrite: `keryx flow ac update <id> --criterion ACn --text "…" --reason "<why>"`. Put the `[verify: …]` marker on every criterion before `flow freeze`. |
@@ -26,7 +26,7 @@ license: "MIT"
26
26
 
27
27
  ## Preflight
28
28
 
29
- Run in the main session: this skill dispatches sub-agents. Without the `Agent` tool, or with the depth budget spent (Claude Code nests at most 3 deep), the whole output is this line, with no tool calls and no work of your own: `STATUS: BLOCKED nested_dispatch_unavailable — run this skill in the main session`
29
+ Run in the main session: this skill dispatches sub-agents. Without a subagent-spawn tool (`Agent`, `spawn_subagent` or the host's equivalent), or with the depth budget spent (Claude Code nests at most 3 deep), the whole output is this line, with no tool calls and no work of your own: `STATUS: BLOCKED nested_dispatch_unavailable — run this skill in the main session`
30
30
 
31
31
  ## Purpose
32
32
 
@@ -1262,8 +1262,8 @@ Three is the self-fix bound `task-implementer` and this skill share
1262
1262
  most achievable gains"* ([arXiv:2607.05197](https://arxiv.org/abs/2607.05197));
1263
1263
  correctness falls **0.820 -> 0.673** across two forced revisions while
1264
1264
  cumulative ever-correct is **0.847**
1265
- ([arXiv:2607.24604](https://arxiv.org/abs/2607.24604)). Aider hardcodes
1266
- `max_reflections = 3`; OpenHands' critic uses 3.
1265
+ ([arXiv:2607.24604](https://arxiv.org/abs/2607.24604)). Other agent tools
1266
+ hardcode the same bound (`max_reflections = 3`).
1267
1267
 
1268
1268
  The bound is a ceiling, not a target. Repetition ends the loop earlier and
1269
1269
  **regardless of remaining iterations** — a counter cannot tell "converging
@@ -22,7 +22,7 @@ Phase 2: Execute plan, bracketing every step with keryx job step:
22
22
  ├─ git worktree add → feature branch (NEVER git checkout -b)
23
23
  ├─ FOR EACH wave → [tests-creator × tasks] then [task-implementer × tasks]
24
24
  ├─ [code-verifier sub-agent] → quality gate
25
- ├─ keryx review start → [review-orchestrator sub-agent] → keryx review ingest
25
+ ├─ keryx review start → [review-orchestrator, inline] → keryx review ingest
26
26
  ├─ IF blocker/major → [task-implementer (fix)] → next managed round (max 3)
27
27
  ├─ keryx review complete --finding … --disposition … --evidence …
28
28
  └─ keryx job document → each step's artifact recorded in the package
@@ -162,7 +162,7 @@ keryx review scope --ref "$BASE_SHA" --json > scope.json
162
162
  keryx review blast-radius --ref "$BASE_SHA" --json > blast-radius.json # KEEP BOTH
163
163
  keryx review stack --json # which reviewers apply
164
164
  keryx review tier --scope <scope> --diff-lines <n> --json # model per dispatch
165
- # dispatch review-orchestrator (skills/gdskills/review/review-orchestrator/SKILL.md);
165
+ # run review-orchestrator inline (skills/gdskills/review/review-orchestrator/SKILL.md);
166
166
  # it runs review-verifier in Wave C and emits the ```json keryx:findings``` block
167
167
  keryx review ingest --report <report.md> --ref <feature-branch> --head <sha> \
168
168
  --scope scope.json --blast-radius blast-radius.json \
@@ -368,8 +368,8 @@ account for most achievable gains"*
368
368
  ([arXiv:2607.05197](https://arxiv.org/abs/2607.05197)); correctness falls
369
369
  **0.820 -> 0.673** across two forced revisions while cumulative ever-correct is
370
370
  **0.847** ([arXiv:2607.24604](https://arxiv.org/abs/2607.24604)) — the agent
371
- finds the fix and then destroys it. Aider hardcodes `max_reflections = 3`;
372
- OpenHands' critic uses 3.
371
+ finds the fix and then destroys it. Other agent tools hardcode the same bound
372
+ (`max_reflections = 3`).
373
373
 
374
374
  **Stop earlier on repetition, whatever the count says.** If an attempt produces
375
375
  the same failure output as the previous attempt — the same failing test with the
@@ -435,8 +435,8 @@ Write full JSON to `<JOBS_ROOT>/<JOB_NAME>/results/<task_id>.json`:
435
435
  Set `skill_drift` from Phase 2.0b: if the project-skill you used was not `fresh`, or the code you wrote diverged from what a skill documents, name the skill and the divergence. The orchestrator uses this to decide whether to trigger `skills learn` (do NOT run `learn` yourself — it is a mutating step the orchestrator dispatches; see `rules/core/skill-lifecycle.mdc`).
436
436
 
437
437
  Three of those fields have a bar, and a field that collects noise trains the
438
- reader to skim all three. Omit one rather than pad it. The trio is adapted (MIT)
439
- from [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) as contract fields rather than a free-text template; the bars below are ours.
438
+ reader to skim all three. Omit one rather than pad it. The trio is adapted (MIT;
439
+ see THIRD_PARTY_NOTICES.md) as contract fields rather than a free-text template; the bars below are ours.
440
440
 
441
441
  - `assumptions` — what you assumed where the task was silent AND acted on. If
442
442
  the assumption being wrong would not change a line you wrote, it is a hedge,
@@ -20,7 +20,7 @@ metadata:
20
20
  version: 1.0.0
21
21
  ---
22
22
 
23
- **Preflight:** run in the main session. Without the `Agent` tool or depth budget, output only `STATUS: BLOCKED nested_dispatch_unavailable — run this skill in the main session`, with no tool calls.
23
+ **Preflight:** run in the main session. Without a subagent-spawn tool (`Agent`, `spawn_subagent` or the host's equivalent) or depth budget, output only `STATUS: BLOCKED nested_dispatch_unavailable — run this skill in the main session`, with no tool calls.
24
24
 
25
25
  # autodoc-orchestrator
26
26
 
@@ -93,8 +93,8 @@ Before the first question, check whether a host can show it: the `ask_user` tool
93
93
 
94
94
  - **Host present:** ask every question through `ask_user`, one per call, with the A/B/C/D options as its options.
95
95
  - **No host:** ask NO question, not even in plain text; nobody is there to answer it. Return `status: "NEEDS_CONTEXT"` with the `assumptions` you would have asked about (question, assumption, `confidence: "assumption"`), `ready_to_proceed: false`. The caller decides.
96
- - A cancelled or empty `ask_user` answer is not an answer: record it as an assumption and stop asking.
97
- - Each `ask_user` question is recorded in the recommendation journal by the tool itself (source, arm, recommendation, answer); you do not record it. `src/decisions/interviewer-path.test.ts` pins this path through the agent's real tool roster.
96
+ - A "No one was asked", cancelled or empty `ask_user` answer is not an answer: record it as an assumption and stop asking.
97
+ - Each `ask_user` question a person answers is recorded in the recommendation journal by the tool itself (source, arm, recommendation, answer); you do not record it. A question the model answered itself under `/mode auto` never reached a person and is not journaled: treat its answer as an assumption, not a decision. `src/decisions/interviewer-path.test.ts` pins this path through the agent's real tool roster.
98
98
 
99
99
  ## Question Bank by Goal Type
100
100
 
@@ -110,7 +110,7 @@ Stop and re-read this skill if you are thinking:
110
110
  |---|---|
111
111
  | "These three questions are related, so I'll ask them in one message to save time." | One question at a time is the rule, and it is not politeness. A batch gets one answer to the easiest item and silence on the rest, which you then record as if it had been answered. |
112
112
  | "The answer was vague, but I understood the gist, so `ready_to_proceed: true`." | A gist is an assumption. Either ask the follow-up, or record the item in `blockers` with `confidence: "assumption"` and leave `ready_to_proceed` false. Proceeding on a gist is exactly the wasted work this gate exists to prevent. |
113
- | "They said 'sure, that sounds about right', so that is a yes and `ready_to_proceed: true`." | A hedge is not a refusal and it is not approval. This is not the vague-answer case above — you understood the answer perfectly, and it committed the user to nothing. Name the hedge back ("that is a 'probably' — is it a yes?") and ask the question that forces one. If no commitment comes, the item is recorded as `confidence: "assumption"`, never as confirmation. (That a hedged agreement is not approval is adapted (MIT) from [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills); naming the hedge back and the confidence enum are ours.) |
113
+ | "They said 'sure, that sounds about right', so that is a yes and `ready_to_proceed: true`." | A hedge is not a refusal and it is not approval. This is not the vague-answer case above — you understood the answer perfectly, and it committed the user to nothing. Name the hedge back ("that is a 'probably' — is it a yes?") and ask the question that forces one. If no commitment comes, the item is recorded as `confidence: "assumption"`, never as confirmation. (That a hedged agreement is not approval is adapted (MIT; see THIRD_PARTY_NOTICES.md); naming the hedge back and the confidence enum are ours.) |
114
114
  | "I inferred the answer from the codebase, so I can mark it `certain`." | `certain` means the user said it. An inference is `assumption`, even a good one — the confidence field is the only signal downstream skills have about which parts of `derived_context` are load-bearing guesses. |
115
115
  | "I've hit the 8-question limit and still don't know, so I'll pick the likely answer." | The limit is a stop rule, not a licence to invent. Remaining unknowns go into `blockers`, `ready_to_proceed` goes false, and the caller decides. |
116
116
  | "Context-collector already ran, so anything still missing must not matter." | Collected context answers what the codebase knows, not what the user intends. Scope, priority and what must NOT change are never in the codebase, and those are the answers that change the approach. |
@@ -221,6 +221,6 @@ Do not report the work as done until all of these hold:
221
221
  upgrade decision for the dependency's owner, and no version was bumped inside
222
222
  this change to obtain it.
223
223
 
224
- Credit: [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills)
225
- (MIT) is where the pairing comes from — check the dependency before calling it,
224
+ Credit: an MIT-licensed skills library (see THIRD_PARTY_NOTICES.md) is where
225
+ the pairing comes from — check the dependency before calling it,
226
226
  and mark what went out unchecked. The ranks and the version rule are ours.
@@ -263,6 +263,6 @@ Do not report the work as done until all of these hold:
263
263
  — a guess dressed as a schedule is neither — and the caller has the
264
264
  before-and-after to migrate unaided, under behaviour unchanged under them.
265
265
 
266
- Credit: [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills)
267
- (MIT) is why this set carries a deprecation skill at all; the five stages and
268
- the notice rule were measured from keryx's own CLI, not taken from there.
266
+ Credit: an MIT-licensed skills library (see THIRD_PARTY_NOTICES.md) is why this
267
+ set carries a deprecation skill at all; the five stages and the notice rule
268
+ were measured from keryx's own CLI, not taken from there.
@@ -185,6 +185,6 @@ A cycle is complete only when all of these hold:
185
185
  - If the cycle found nothing, it says so plainly, with what was checked and what
186
186
  was left unreached. `STATUS: NO FINDINGS` is a pass, not an incomplete run.
187
187
 
188
- Credit: [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills)
189
- (MIT) is where the in-flight doubt pass comes from: a reader gets the artifact
188
+ Credit: an MIT-licensed skills library (see THIRD_PARTY_NOTICES.md) is where
189
+ the in-flight doubt pass comes from: a reader gets the artifact
190
190
  and the contract, never the author's reasoning. The bar and the bound are ours.
@@ -77,7 +77,7 @@ Flag heavy dependencies:
77
77
  ## Rules
78
78
 
79
79
  - Don't make changes — only analyze and report
80
- - An optimisation that measures neutral is reported with revert as its recommended fix, not as harmless. The burden is on keeping it, never on dropping it: it leaves the code more complicated than it found it, and it survives review precisely because nothing is wrong with it. This rule and the ledger two bullets down are adapted (MIT) from [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills); the noise definition and the groundwork exception are ours
80
+ - An optimisation that measures neutral is reported with revert as its recommended fix, not as harmless. The burden is on keeping it, never on dropping it: it leaves the code more complicated than it found it, and it survives review precisely because nothing is wrong with it. This rule and the ledger two bullets down are adapted (MIT; see THIRD_PARTY_NOTICES.md); the noise definition and the groundwork exception are ours
81
81
  - Neutral is a delta that does not clear the noise. Take the before reading three times on one named workload and keep the spread; any after-delta inside that spread is neutral. Indistinguishable from zero is the same verdict as zero, not a smaller win — say which it was, and how many runs said so
82
82
  - Record the attempt in the flow journal (`.metaproject/flows/<flow>/journal.md`), in the same change as the revert: what was tried, the before and after with the command and the workload that produced them, and why it did not help *here*. "Tried caching, didn't help" is worse than no entry — it forecloses the idea for the next agent and leaves them nothing to overturn it with
83
83
  - Groundwork is the only exception: a neutral change survives when the change it is a prerequisite for is in the same branch and the two measure a win together. A payoff promised for later is not a measurement — revert, and record what the pair would have to show
@@ -199,6 +199,6 @@ Report the defect fixed only when all of these hold:
199
199
  the evidence and its class, the surviving hypotheses with their killing
200
200
  observations, and the instrumentation that would settle it.
201
201
 
202
- Credit: [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills)
203
- (MIT) is why this set carries a debugging skill at all; the step order, the
202
+ Credit: an MIT-licensed skills library (see THIRD_PARTY_NOTICES.md) is why
203
+ this set carries a debugging skill at all; the step order, the
204
204
  evidence classes and the non-reproduction protocol were written here, not taken.
@@ -348,21 +348,7 @@ STATUS: DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED
348
348
 
349
349
  ## Job Context Awareness
350
350
 
351
- When dispatched by `review-orchestrator` or `job-orchestrator`, the prompt MAY include:
352
-
353
- ```
354
- JOB_NAME: <job-name>
355
- CONTEXT_PATH: .metaproject/jobs/<job-name>/ai/context.md
356
- ```
357
-
358
- If provided and the file exists, read the context document **before** reviewing the diff.
359
- Use it to:
360
- - Understand which architectural patterns and layers were intentionally chosen
361
- - Avoid flagging deliberate deviations from the default layer model as violations
362
- - Identify framework and library choices that affect what counts as a valid pattern
363
-
364
- If absent, proceed normally — context is optional and non-blocking.
365
- When in doubt about whether a pattern was intentional, use `info` severity and note the uncertainty.
351
+ When dispatched by `review-orchestrator` or `job-orchestrator`, the prompt MAY carry `JOB_NAME` and `CONTEXT_PATH: .metaproject/jobs/<job-name>/ai/context.md`. If given and the file exists, read it before you start; use it for intentional design decisions (do not flag correct library usage), acceptance criteria when there is no `issue_url`, and the severity of edge cases the team accepted. If absent, proceed normally: context is optional and non-blocking.
366
352
 
367
353
  ---
368
354
 
@@ -377,3 +363,16 @@ When in doubt about whether a pattern was intentional, use `info` severity and n
377
363
  | "The team probably didn't intend this pattern" | Unless the context doc or a clear rule says otherwise, assume intentional; use `info` if unsure |
378
364
  | "NestJS global module is always wrong" | `@Global()` is a violation only when used for non-global concerns; framework-provided globals are fine |
379
365
  | "I can't determine if this is wrong without knowing the full architecture" | Use `NEEDS_CONTEXT` status and ask one specific question |
366
+
367
+ <!-- keryx:layers-block:start -->
368
+ ## Project layers: graph, tests, wiki, memory
369
+
370
+ Your payload may carry a `layers` block for your files: the files the graph says are affected, symbol neighbours, related tests, the wiki pages that describe the files and the memory entries for their module. It is bounded, and `truncated` names what was cut and the command that fetches the rest. When the diff alone does not settle a question, ask the project. Each command below is read-only and calls no model.
371
+
372
+ - `keryx gdgraph affected <file>`: who depends on the file. Use it before you call a change contained, and whenever a changed export, signature or behaviour may have callers outside your files.
373
+ - `keryx test related <file>`: the tests that cover the file. Use it before you say a behaviour is untested, and to judge whether a test would fail if the code broke.
374
+ - `keryx wiki ask "<q>"`: what the project wiki says a module is for and which rules bind it. Use it when the intent of the code is unclear and the diff does not state it.
375
+ - `keryx memory search "<q>"`: recorded lessons, decisions and known mistakes. Use it before you report a pattern the project may already have decided on, and when a failure looks like one it has seen.
376
+
377
+ In a review package, `keryx gdgraph affected <file> --data-dir <package>/layers/graph` reads the graph built at the PR head; the tracked graph may be from another commit. A finding still rests on the code you read: what a layer says is a lead, not evidence. If a layer is empty or a command fails, say so in the report instead of guessing.
378
+ <!-- keryx:layers-block:end -->
@@ -338,20 +338,7 @@ STATUS: DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED
338
338
 
339
339
  ## Job Context Awareness
340
340
 
341
- When dispatched by `job-orchestrator` or called with an explicit context path, the prompt MAY include:
342
-
343
- ```
344
- JOB_NAME: <job-name>
345
- CONTEXT_PATH: <JOBS_ROOT>/<job-name>/ai/context.md
346
- ```
347
-
348
- If provided and the file exists, read the context document **before** running scope detection.
349
- Use it to understand:
350
- - Which ORM, validation library, or guard strategy was intentionally chosen
351
- - Project-level architectural decisions (e.g., no repository pattern by design)
352
- - Acceptance criteria to verify spec compliance
353
-
354
- If absent, proceed normally — context is optional and non-blocking.
341
+ When dispatched by `review-orchestrator` or `job-orchestrator`, the prompt MAY carry `JOB_NAME` and `CONTEXT_PATH: .metaproject/jobs/<job-name>/ai/context.md`. If given and the file exists, read it before you start; use it for intentional design decisions (do not flag correct library usage), acceptance criteria when there is no `issue_url`, and the severity of edge cases the team accepted. If absent, proceed normally: context is optional and non-blocking.
355
342
 
356
343
  ---
357
344
 
@@ -364,3 +351,16 @@ If absent, proceed normally — context is optional and non-blocking.
364
351
  | "The controller has some logic but it's minor" | The line is binary — logic in controller = untestable without HTTP stack |
365
352
  | "Stack trace in response only shows in dev" | Config can be wrong in prod; treat it as blocker always |
366
353
  | "I'll skip the migration check — it's just a column rename" | Column renames without fallback break zero-downtime deploys |
354
+
355
+ <!-- keryx:layers-block:start -->
356
+ ## Project layers: graph, tests, wiki, memory
357
+
358
+ Your payload may carry a `layers` block for your files: the files the graph says are affected, symbol neighbours, related tests, the wiki pages that describe the files and the memory entries for their module. It is bounded, and `truncated` names what was cut and the command that fetches the rest. When the diff alone does not settle a question, ask the project. Each command below is read-only and calls no model.
359
+
360
+ - `keryx gdgraph affected <file>`: who depends on the file. Use it before you call a change contained, and whenever a changed export, signature or behaviour may have callers outside your files.
361
+ - `keryx test related <file>`: the tests that cover the file. Use it before you say a behaviour is untested, and to judge whether a test would fail if the code broke.
362
+ - `keryx wiki ask "<q>"`: what the project wiki says a module is for and which rules bind it. Use it when the intent of the code is unclear and the diff does not state it.
363
+ - `keryx memory search "<q>"`: recorded lessons, decisions and known mistakes. Use it before you report a pattern the project may already have decided on, and when a failure looks like one it has seen.
364
+
365
+ In a review package, `keryx gdgraph affected <file> --data-dir <package>/layers/graph` reads the graph built at the PR head; the tracked graph may be from another commit. A finding still rests on the code you read: what a layer says is a lead, not evidence. If a layer is empty or a command fails, say so in the report instead of guessing.
366
+ <!-- keryx:layers-block:end -->
@@ -594,22 +594,7 @@ Stop and re-read these rules if you are thinking:
594
594
 
595
595
  ## Job Context Awareness
596
596
 
597
- When dispatched by `job-orchestrator` or `review-orchestrator` as part of a job pipeline, the prompt MAY include:
598
-
599
- ```
600
- JOB_NAME: <job-name>
601
- CONTEXT_PATH: <JOBS_ROOT>/<job-name>/ai/context.md
602
- ```
603
-
604
- If provided and the file exists, read the context document before starting the review. Use it to:
605
- - Understand which MobX version and decorator configuration the project uses (legacy decorators vs. `makeObservable`)
606
- - Identify custom base store classes or lifecycle conventions that differ from defaults
607
- - Understand which accessibility standard the project targets (WCAG 2.1 AA vs. custom)
608
- - Identify intentional patterns that appear to violate rules but are documented exceptions
609
- - Validate findings against documented project-specific conventions
610
-
611
- If the file does not exist or is not provided, proceed normally — context is optional and non-blocking.
612
-
597
+ When dispatched by `review-orchestrator` or `job-orchestrator`, the prompt MAY carry `JOB_NAME` and `CONTEXT_PATH: .metaproject/jobs/<job-name>/ai/context.md`. If given and the file exists, read it before you start; use it for intentional design decisions (do not flag correct library usage), acceptance criteria when there is no `issue_url`, and the severity of edge cases the team accepted. If absent, proceed normally: context is optional and non-blocking.
613
598
 
614
599
  ## Orchestrated Review Contract
615
600
 
@@ -617,3 +602,15 @@ When dispatched by `review-orchestrator`, follow the provided `reviewer-input.sc
617
602
 
618
603
  ---
619
604
 
605
+ <!-- keryx:layers-block:start -->
606
+ ## Project layers: graph, tests, wiki, memory
607
+
608
+ Your payload may carry a `layers` block for your files: the files the graph says are affected, symbol neighbours, related tests, the wiki pages that describe the files and the memory entries for their module. It is bounded, and `truncated` names what was cut and the command that fetches the rest. When the diff alone does not settle a question, ask the project. Each command below is read-only and calls no model.
609
+
610
+ - `keryx gdgraph affected <file>`: who depends on the file. Use it before you call a change contained, and whenever a changed export, signature or behaviour may have callers outside your files.
611
+ - `keryx test related <file>`: the tests that cover the file. Use it before you say a behaviour is untested, and to judge whether a test would fail if the code broke.
612
+ - `keryx wiki ask "<q>"`: what the project wiki says a module is for and which rules bind it. Use it when the intent of the code is unclear and the diff does not state it.
613
+ - `keryx memory search "<q>"`: recorded lessons, decisions and known mistakes. Use it before you report a pattern the project may already have decided on, and when a failure looks like one it has seen.
614
+
615
+ In a review package, `keryx gdgraph affected <file> --data-dir <package>/layers/graph` reads the graph built at the PR head; the tracked graph may be from another commit. A finding still rests on the code you read: what a layer says is a lead, not evidence. If a layer is empty or a command fails, say so in the report instead of guessing.
616
+ <!-- keryx:layers-block:end -->