simt-dev 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +169 -80
  3. package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
  4. package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
  5. package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
  6. package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
  7. package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
  8. package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
  9. package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
  10. package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
  11. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
  12. package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
  13. package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
  14. package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
  15. package/ecosystems/bun.json +6 -0
  16. package/ecosystems/cargo.json +25 -0
  17. package/ecosystems/composer.json +6 -0
  18. package/ecosystems/dart.json +24 -0
  19. package/ecosystems/deno.json +21 -0
  20. package/ecosystems/devcontainers.json +6 -0
  21. package/ecosystems/docker-compose.json +6 -0
  22. package/ecosystems/docker.json +6 -0
  23. package/ecosystems/dotnet.json +25 -0
  24. package/ecosystems/elixir.json +23 -0
  25. package/ecosystems/gitsubmodule.json +6 -0
  26. package/ecosystems/go.json +25 -0
  27. package/ecosystems/gradle.json +13 -0
  28. package/ecosystems/maven.json +23 -0
  29. package/ecosystems/node.json +25 -0
  30. package/ecosystems/python.json +20 -0
  31. package/ecosystems/ruby.json +23 -0
  32. package/ecosystems/swift.json +25 -0
  33. package/ecosystems/terraform.json +6 -0
  34. package/ecosystems/uv.json +6 -0
  35. package/harnesses/antigravity.json +162 -0
  36. package/harnesses/claude.json +206 -0
  37. package/harnesses/codex.json +136 -0
  38. package/harnesses/copilot.json +88 -0
  39. package/harnesses/cursor.json +140 -0
  40. package/harnesses/deepseek.json +99 -0
  41. package/harnesses/gemini.json +122 -0
  42. package/harnesses/opencode.json +173 -0
  43. package/package.json +3 -1
  44. package/rules/mdc/intake-questions.mdc +8 -0
  45. package/src/advice/advice.mjs +3 -2
  46. package/src/agenthooks/render.mjs +86 -180
  47. package/src/agents/render.mjs +15 -18
  48. package/src/api.mjs +7 -5
  49. package/src/argv.mjs +19 -0
  50. package/src/bench/ablate.mjs +3 -4
  51. package/src/bench/adapters.json +155 -113
  52. package/src/bench/adapters.mjs +466 -0
  53. package/src/bench/arms.mjs +333 -0
  54. package/src/bench/grade.mjs +12 -26
  55. package/src/bench/graders.mjs +42 -0
  56. package/src/bench/mine.mjs +17 -535
  57. package/src/bench/probe-ledger.mjs +149 -0
  58. package/src/bench/probes.catalog.mjs +918 -0
  59. package/src/bench/probes.mjs +15 -1037
  60. package/src/bench/render.mjs +111 -0
  61. package/src/bench/report.mjs +116 -971
  62. package/src/bench/run.mjs +164 -881
  63. package/src/bench/variants.mjs +4 -5
  64. package/src/bench/workspace.mjs +160 -0
  65. package/src/brief/assemble.mjs +11 -13
  66. package/src/ci/dependabot.mjs +25 -55
  67. package/src/commands/agent.mjs +1 -9
  68. package/src/commands/agenthook.mjs +66 -35
  69. package/src/commands/bench.mjs +2 -2
  70. package/src/commands/ci.mjs +11 -13
  71. package/src/commands/docs.mjs +88 -0
  72. package/src/commands/gate.mjs +10 -13
  73. package/src/commands/harness.mjs +3 -3
  74. package/src/commands/help.mjs +44 -3
  75. package/src/commands/hooks.mjs +20 -20
  76. package/src/commands/ignore.mjs +11 -7
  77. package/src/commands/impact.mjs +4 -8
  78. package/src/commands/index.mjs +49 -9
  79. package/src/commands/init-scan.mjs +90 -0
  80. package/src/commands/init.mjs +122 -104
  81. package/src/commands/lease.mjs +67 -18
  82. package/src/commands/map.mjs +10 -5
  83. package/src/commands/mcp.mjs +2 -17
  84. package/src/commands/repoindex.mjs +8 -8
  85. package/src/commands/rules.mjs +8 -4
  86. package/src/commands/shared.mjs +12 -16
  87. package/src/commands/skills.mjs +5 -6
  88. package/src/commands/skillscompile.mjs +3 -3
  89. package/src/commands/skillsinstall.mjs +5 -4
  90. package/src/commands/slice.mjs +199 -64
  91. package/src/commands/spec.mjs +78 -62
  92. package/src/commands/status.mjs +24 -12
  93. package/src/commands/sync.mjs +14 -14
  94. package/src/commands/tests.mjs +12 -12
  95. package/src/commands/tokens.mjs +19 -24
  96. package/src/commands/upgrade.mjs +8 -4
  97. package/src/commands/why.mjs +6 -13
  98. package/src/compare/hooksstate.mjs +2 -6
  99. package/src/compare/learn.mjs +3 -1
  100. package/src/decisions/index.mjs +3 -2
  101. package/src/docs/commands.mjs +65 -0
  102. package/src/ecosystem/records.mjs +219 -0
  103. package/src/explain.mjs +31 -0
  104. package/src/gates/index.mjs +385 -0
  105. package/src/graph/compile.mjs +8 -4
  106. package/src/graph/load.mjs +2 -1
  107. package/src/graph/spec.mjs +10 -8
  108. package/src/harness/records.mjs +143 -0
  109. package/src/hooks/capability.mjs +5 -3
  110. package/src/hooks/dispatcher.mjs +26 -32
  111. package/src/hooks/gates.mjs +12 -2
  112. package/src/hooks/resolve.mjs +2 -7
  113. package/src/hooks/where.mjs +2 -7
  114. package/src/identity.mjs +2 -11
  115. package/src/index/coverage.mjs +58 -0
  116. package/src/index/extract.mjs +6 -16
  117. package/src/index/gate.mjs +1 -1
  118. package/src/index/govern.mjs +26 -9
  119. package/src/index/impact.mjs +4 -8
  120. package/src/index/map.mjs +14 -15
  121. package/src/index/open.mjs +173 -0
  122. package/src/index/persist.mjs +224 -57
  123. package/src/index/store.mjs +82 -39
  124. package/src/index/tests.mjs +2 -6
  125. package/src/journal.mjs +16 -11
  126. package/src/kernel/card.mjs +37 -63
  127. package/src/kernel/checks.mjs +4 -10
  128. package/src/lease.mjs +102 -7
  129. package/src/ledger/index.mjs +124 -17
  130. package/src/mcp/paginate.mjs +33 -0
  131. package/src/mcp/schemas.mjs +512 -0
  132. package/src/mcp/server.mjs +49 -9
  133. package/src/mcp/tools-facts.mjs +326 -0
  134. package/src/mcp/tools-read.mjs +221 -0
  135. package/src/mcp/tools.mjs +4 -1005
  136. package/src/{bench → measure}/corpus.mjs +1 -1
  137. package/src/measure/metrics.mjs +236 -0
  138. package/src/measure/records.mjs +211 -0
  139. package/src/measure/stats.mjs +538 -0
  140. package/src/measure/task-schema.mjs +125 -0
  141. package/src/measure/tasks.mjs +69 -0
  142. package/src/memory/decide.mjs +6 -15
  143. package/src/memory/lesson-text.mjs +61 -0
  144. package/src/memory/lessons.mjs +29 -82
  145. package/src/name.mjs +7 -0
  146. package/src/output/index.mjs +122 -31
  147. package/src/packs/manage.mjs +15 -9
  148. package/src/plan/index.mjs +1 -1
  149. package/src/plugins.mjs +22 -16
  150. package/src/prbody.mjs +7 -11
  151. package/src/projection/loaders.mjs +7 -692
  152. package/src/projection/manifest.mjs +10 -435
  153. package/src/projection/mdc.mjs +7 -8
  154. package/src/projection/project.mjs +126 -221
  155. package/src/projection/region.mjs +561 -0
  156. package/src/projection/rules.mjs +16 -547
  157. package/src/projection/surfaces.mjs +43 -210
  158. package/src/projection/targets.mjs +5 -5
  159. package/src/repo-root.mjs +13 -1
  160. package/src/resolve/eval.mjs +2 -1
  161. package/src/resolve/fingerprint.mjs +0 -7
  162. package/src/resolve/resolve.mjs +1 -41
  163. package/src/review/critic.mjs +8 -12
  164. package/src/run.mjs +98 -0
  165. package/src/secrets.mjs +14 -22
  166. package/src/state/events.mjs +129 -0
  167. package/src/status-snapshot.mjs +233 -0
  168. package/src/status.mjs +2 -6
  169. package/src/sys/git.mjs +82 -0
  170. package/src/{bench/process.mjs → sys/proc.mjs} +28 -5
  171. package/src/testing/parsers.mjs +265 -0
  172. package/src/testing/patches.mjs +23 -0
  173. package/src/testing/paths.mjs +49 -0
  174. package/src/testing/runners.mjs +186 -0
  175. package/src/testing/vault.mjs +68 -0
  176. package/src/text/frontmatter.mjs +49 -0
  177. package/src/text/tokens.mjs +52 -0
  178. package/src/toolwords.mjs +1 -1
  179. package/src/util/fs.mjs +20 -0
  180. package/src/util/sort.mjs +12 -0
  181. package/src/workspace.mjs +3 -1
  182. package/src/write/plan.mjs +82 -0
  183. package/templates/plan/tracker.json +1 -0
  184. package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
  185. package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
  186. package/src/bench/tasks.mjs +0 -36
@@ -0,0 +1,122 @@
1
+ {
2
+ "id": "gemini",
3
+ "loaderOrder": 6,
4
+ "loader": {
5
+ "id": "gemini",
6
+ "label": "Gemini CLI",
7
+ "readers": "Gemini CLI (google-gemini/gemini-cli)",
8
+ "surface": null,
9
+ "start": [
10
+ {
11
+ "scope": "project",
12
+ "path": "GEMINI.md",
13
+ "tree": "chain",
14
+ "atStart": true,
15
+ "why": "Gemini CLI context docs (docs/cli/gemini-md/): GEMINI.md is collected from the working directory upward through parent directories until the first directory holding a boundary marker (.git by default) or home"
16
+ },
17
+ {
18
+ "scope": "user",
19
+ "path": "~/.gemini/GEMINI.md",
20
+ "tree": "single",
21
+ "atStart": true,
22
+ "why": "Gemini CLI context docs: the user-global ~/.gemini/GEMINI.md opens the concatenation, before any project file"
23
+ }
24
+ ],
25
+ "alsoReads": [
26
+ {
27
+ "what": "the context file under another name — settings.json context.fileName, a string or an array",
28
+ "atStart": true,
29
+ "why": "Gemini CLI configuration docs: context.fileName renames the context file (a string or an array), so an installation listing AGENTS.md there reads AGENTS.md as its GEMINI.md"
30
+ },
31
+ {
32
+ "what": "GEMINI.md (or the renamed context file) in subdirectories below the working directory",
33
+ "atStart": false,
34
+ "why": "Gemini CLI context docs: directories a tool touches are scanned for a context file up to a trusted root, bounded by context.discoveryMaxDirs (200) - not at session start"
35
+ }
36
+ ],
37
+ "order": "user-global ~/.gemini/GEMINI.md first, then the project chain general to specific - project-root side first (upward traversal stops at the first directory holding a boundary marker, .git by default) down through each directory to the working directory - then just-in-time files a tool touch discovered; the whole result is one concatenation with origin separators",
38
+ "cap": null,
39
+ "capNote": "no byte cap is published for the merged Gemini CLI context (checked 2026-10-26)",
40
+ "imports": {
41
+ "syntax": "@path",
42
+ "depth": 5,
43
+ "why": "Gemini CLI context docs: @path imports resolve relative or absolute to a maximum depth of 5, with cycles prevented by skipping files already on the stack"
44
+ },
45
+ "nested": "general to specific, all loaded — unlike Codex, every context file on the walk contributes; a nearer file cannot hide a parent one, it just concatenates after it; subdirectory files wait for a tool touch",
46
+ "skills": [],
47
+ "skillsNote": "no project skill directory is documented for Gemini CLI; extensions are the distribution mechanism, and they are tooling not context",
48
+ "hooks": {
49
+ "path": ".gemini/settings.json",
50
+ "verified": "Gemini CLI hooks reference (geminicli.com/docs/hooks/reference/, read 2026-10-26): a project .gemini/settings.json carries a top-level hooks object whose events are arrays of {matcher, hooks:[{type:\"command\",command,timeout? (milliseconds)}]} — the same shape Claude and Codex use; BeforeTool denies with {decision:\"deny\",reason}, BeforeAgent and AfterTool inject via hookSpecificOutput.additionalContext"
51
+ },
52
+ "agents": {
53
+ "dir": ".gemini/agents",
54
+ "ext": ".md",
55
+ "verified": "Gemini CLI subagents docs (geminicli.com/docs/core/subagents, read 2026-10-30): project subagents are markdown files with name and description frontmatter under .gemini/agents/"
56
+ },
57
+ "mcp": {
58
+ "path": ".gemini/settings.json",
59
+ "key": "mcpServers",
60
+ "verified": "Gemini CLI configuration docs (docs/reference/configuration/, read 2026-10-26): a project .gemini/settings.json (a user ~/.gemini/settings.json merged behind it, objects merged key by key) carries a top-level mcpServers object whose entries name a command, a url or an httpUrl"
61
+ },
62
+ "verified": "Gemini CLI context and configuration docs (geminicli.com/docs/cli/gemini-md/ and docs/reference/configuration/, read 2026-10-26) — settings live at project .gemini/settings.json and user ~/.gemini/settings.json, merged for objects; the context is user ~/.gemini/GEMINI.md plus the context file (GEMINI.md unless context.fileName renames it, an array is allowed) from the working directory upward through parents to the first .git or home, with @path imports to depth 5; server registration is the mcpServers object in those same settings files; no byte cap is published",
63
+ "note": "The configuration reference this table once recorded as 404ing is live again as of 2026-10-26, which is why the row is full instead of thin. What still shapes a byte count is the pair of absences the docs confirm rather than deny: no cap, and no promise that AGENTS.md is read at all — context.fileName decides that, and it may name anything."
64
+ },
65
+ "hooks": {
66
+ "id": "gemini",
67
+ "event": "BeforeTool",
68
+ "matcher": "run_shell_command|write_file|replace",
69
+ "timeout": 10000,
70
+ "denyExit": 0,
71
+ "verified": "geminicli.com/docs/hooks/reference/, read 2026-10-26",
72
+ "prompt": {
73
+ "event": "BeforeAgent",
74
+ "verified": "geminicli.com/docs/hooks/reference/, read 2026-10-26",
75
+ "delivery": {
76
+ "hookSpecificOutput": {
77
+ "additionalContext": "__SIMT_TEXT__"
78
+ }
79
+ }
80
+ },
81
+ "post": {
82
+ "event": "AfterTool",
83
+ "verified": "geminicli.com/docs/hooks/reference/, read 2026-10-26",
84
+ "delivery": {
85
+ "hookSpecificOutput": {
86
+ "additionalContext": "__SIMT_TEXT__"
87
+ }
88
+ }
89
+ },
90
+ "deny": {
91
+ "decision": "deny",
92
+ "reason": "__SIMT_TEXT__"
93
+ }
94
+ },
95
+ "payload": {
96
+ "toolKey": "tool_name",
97
+ "inputKey": "tool_input",
98
+ "shellTools": [
99
+ "run_shell_command"
100
+ ],
101
+ "commandKey": "command",
102
+ "writeTools": [
103
+ "write_file",
104
+ "replace"
105
+ ],
106
+ "pathKeys": [
107
+ "file_path"
108
+ ],
109
+ "rootKeys": [
110
+ [
111
+ "cwd"
112
+ ]
113
+ ]
114
+ },
115
+ "pluginOrder": 2,
116
+ "plugin": {
117
+ "rootVar": "extensionPath",
118
+ "projectVar": null,
119
+ "hooksPath": "hooks/hooks.json",
120
+ "runtimeDir": "."
121
+ }
122
+ }
@@ -0,0 +1,173 @@
1
+ {
2
+ "id": "opencode",
3
+ "loaderOrder": 1,
4
+ "surfaceOrder": 1,
5
+ "surface": {
6
+ "id": "opencode",
7
+ "label": "opencode (AGENTS.md + .agents/skills)",
8
+ "readers": "OpenCode v2, and AGENTS.md is also read natively by Codex, Cursor, Windsurf, Zed, Amp, Jules, Copilot CLI and the DeepSeek harness; Claude Code falls back to it, Antigravity and Gemini CLI accept GEMINI.md as an equal name",
9
+ "primary": "AGENTS.md",
10
+ "kind": "region",
11
+ "rule": null,
12
+ "foldRules": true,
13
+ "readsAgentsMd": "all",
14
+ "configDirs": [
15
+ ".opencode"
16
+ ],
17
+ "skills": ".agents/skills",
18
+ "documentedSkillsPaths": [
19
+ ".opencode/skills",
20
+ ".claude/skills",
21
+ ".agents/skills"
22
+ ],
23
+ "readLimits": [
24
+ {
25
+ "reader": "Codex",
26
+ "bytes": 32768,
27
+ "verified": "Codex project_doc_max_bytes defaults to 32 KiB"
28
+ }
29
+ ],
30
+ "verified": "OpenCode v2 instructions and skills docs (opencode.ai/v2/docs, read 2026-10-14) — AGENTS.md only, combined from the workspace up to the project root; CLAUDE.md is not a fallback; skills at .opencode/skills/, .claude/skills/ and .agents/skills/<name>/SKILL.md, with the model shown name and description",
31
+ "note": ".agents/skills is one of the three skill locations opencode documents, so the same directory serves the standard surface without a symlink."
32
+ },
33
+ "loader": {
34
+ "id": "opencode",
35
+ "label": "OpenCode v2",
36
+ "readers": "OpenCode v2",
37
+ "surface": "opencode",
38
+ "start": [
39
+ {
40
+ "scope": "project",
41
+ "path": "AGENTS.md",
42
+ "tree": "chain",
43
+ "atStart": true,
44
+ "why": "OpenCode v2 instructions docs: every AGENTS.md from the workspace directory up to the project root is loaded and combined; V2 recognizes AGENTS.md only and does not fall back to CLAUDE.md"
45
+ },
46
+ {
47
+ "scope": "user",
48
+ "path": "~/.config/opencode/AGENTS.md",
49
+ "tree": "single",
50
+ "atStart": true,
51
+ "why": "OpenCode v2 instructions docs: the global file is loaded first, before the project chain"
52
+ },
53
+ {
54
+ "scope": "project",
55
+ "path": ".claude/skills/{name}/SKILL.md",
56
+ "tree": "nested",
57
+ "dir": ".claude/skills",
58
+ "suffix": "/SKILL.md",
59
+ "atStart": true,
60
+ "metadata": true,
61
+ "why": "OpenCode v2 skills docs: .claude/skills is a project compatibility source, registered before .agents/skills"
62
+ },
63
+ {
64
+ "scope": "project",
65
+ "path": ".agents/skills/{name}/SKILL.md",
66
+ "tree": "nested",
67
+ "dir": ".agents/skills",
68
+ "suffix": "/SKILL.md",
69
+ "atStart": true,
70
+ "metadata": true,
71
+ "why": "OpenCode v2 skills docs: .agents/skills is a project compatibility source; the model is shown the id, name and description, not the body"
72
+ },
73
+ {
74
+ "scope": "project",
75
+ "path": ".opencode/skills/{name}/SKILL.md",
76
+ "tree": "nested",
77
+ "dir": ".opencode/skills",
78
+ "suffix": "/SKILL.md",
79
+ "atStart": true,
80
+ "metadata": true,
81
+ "why": "OpenCode v2 skills docs: .opencode/skills is the project source and outranks .agents/skills and .claude/skills for the same id"
82
+ }
83
+ ],
84
+ "alsoReads": [
85
+ {
86
+ "what": "AGENTS.md below the workspace",
87
+ "atStart": false,
88
+ "why": "OpenCode v2 instructions docs: instruction files below the workspace load when the agent reads or lists that area, not at session start"
89
+ },
90
+ {
91
+ "what": "opencode.json `instructions` array",
92
+ "atStart": false,
93
+ "why": "OpenCode v2 instructions docs: the config schema accepts an instructions array, but V2 does not resolve its files, globs or URLs"
94
+ }
95
+ ],
96
+ "order": "global ~/.config/opencode/AGENTS.md, then project AGENTS.md files from the workspace directory up to the project root (nearest first); files are combined, not overridden",
97
+ "cap": null,
98
+ "capNote": "no byte cap published for AGENTS.md; V2 accepts an instructions array and does not load it, so there is no fetch timeout to apply",
99
+ "imports": {
100
+ "syntax": null,
101
+ "depth": 0,
102
+ "why": "OpenCode v2 instructions docs: an AGENTS.md reference to another file is not an include; V2 loads AGENTS.md files by discovery, not by @path expansion"
103
+ },
104
+ "nested": "cumulative up to the project root at session start; files below the workspace load when that area is read",
105
+ "skills": [
106
+ ".agents/skills",
107
+ ".opencode/skills",
108
+ ".claude/skills"
109
+ ],
110
+ "hooks": {
111
+ "path": ".opencode/plugins/simt-dev.js",
112
+ "verified": "OpenCode v2 plugins docs: direct .ts and .js files and immediate plugin packages are loaded from every discovered .opencode/plugins/ directory, and the plugins array in opencode.json(c)"
113
+ },
114
+ "agents": {
115
+ "dir": ".opencode/agents",
116
+ "ext": ".md",
117
+ "verified": "OpenCode v2 agents docs (opencode.ai/v2/docs/agents, read 2026-10-30): subagents are markdown files with a description frontmatter under .opencode/agents/"
118
+ },
119
+ "mcp": {
120
+ "path": "opencode.json",
121
+ "key": "mcp.servers",
122
+ "verified": "OpenCode v2 MCP servers docs (read 2026-10-14): servers are named under mcp.servers; V2 does not place server names directly under mcp"
123
+ },
124
+ "verified": "OpenCode v2 instructions, skills, plugins and MCP docs (opencode.ai/v2/docs, read 2026-10-14) — V2 loads AGENTS.md only, combining the global file with every project AGENTS.md from the workspace up to the project root; CLAUDE.md is not a fallback; the instructions array is accepted and not loaded; skills advertise name and description from .opencode/skills, .claude/skills and .agents/skills",
125
+ "note": "This row is OpenCode v2. V1 read CLAUDE.md when AGENTS.md was absent and loaded the instructions array; V2 does neither, so a meter that still did would bill bytes this reader does not pay."
126
+ },
127
+ "hooks": {
128
+ "id": "opencode",
129
+ "event": "execute.before",
130
+ "matcher": "shell|write|edit",
131
+ "file": true,
132
+ "denyExit": 2,
133
+ "verified": "opencode.ai/v2/docs/build/plugins/server, read 2026-10-29",
134
+ "prompt": {
135
+ "event": "prompt",
136
+ "verified": "opencode.ai/v2/docs/build/plugins/server, read 2026-10-29",
137
+ "delivery": {
138
+ "text": "__SIMT_TEXT__"
139
+ }
140
+ },
141
+ "post": null,
142
+ "deny": {
143
+ "reason": "__SIMT_TEXT__"
144
+ }
145
+ },
146
+ "payload": {
147
+ "toolKey": "tool",
148
+ "inputKey": "input",
149
+ "shellTools": [
150
+ "shell"
151
+ ],
152
+ "commandKey": "command",
153
+ "writeTools": [
154
+ "write",
155
+ "edit"
156
+ ],
157
+ "pathKeys": [
158
+ "path"
159
+ ],
160
+ "rootKeys": [
161
+ [
162
+ "cwd"
163
+ ]
164
+ ]
165
+ },
166
+ "pluginOrder": 4,
167
+ "plugin": {
168
+ "rootVar": null,
169
+ "projectVar": null,
170
+ "hooksPath": "server.js",
171
+ "runtimeDir": "."
172
+ }
173
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simt-dev",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "An intelligence amplifier for AI coding agents: rules and dual gates adopted by one command, plus local retrieval, task briefs, benches and repo memory — deterministic by default, with local-model adapters that propose and never select.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Said Elimam",
@@ -27,6 +27,8 @@
27
27
  "templates",
28
28
  "agents",
29
29
  "skills",
30
+ "ecosystems",
31
+ "harnesses",
30
32
  "docs/decisions",
31
33
  "CHANGELOG.md"
32
34
  ],
@@ -41,6 +41,14 @@ the list to reach it, and never split one decision into several questions to fit
41
41
  - If that is not available, present numbered choices with `(recommended)` on the first option.
42
42
  - Do not ask operational or procedural choices; apply the best documented option.
43
43
 
44
+ ## Small work (no plan file)
45
+
46
+ When the whole of the work is small — fewer than three slices — do not create a plan file.
47
+ Plan it and implement it directly, in a single slice: state the slice and its acceptance
48
+ checks, resolve any material decisions, and build it. The `{{plansGlob}}` tracker table and
49
+ its `NNNN-slug.md` deliverable are for work that spans multiple slices; a single slice has
50
+ nothing to track. The slice-commit procedure still applies to the one commit it produces.
51
+
44
52
  ## Required First Turn
45
53
 
46
54
  1. State the smallest independently valuable slice, the owning repo, the active plan
@@ -21,8 +21,9 @@
21
21
  // records or a prices table price the tokens, total tokens otherwise. Ordering by a number
22
22
  // that does not exist for any run would be nondeterminism, not economics.
23
23
  import { classifyTask } from '../brief/kind.mjs';
24
- import { computeRunCost, isRunResolved, loadPrices, loadRunRecords } from '../bench/report.mjs';
25
- import { loadTasks } from '../bench/tasks.mjs';
24
+ import { computeRunCost, isRunResolved, loadPrices } from '../measure/metrics.mjs';
25
+ import { loadRunRecords } from '../measure/records.mjs';
26
+ import { loadTasks } from '../measure/tasks.mjs';
26
27
  import { CONFIG_DIR } from '../name.mjs';
27
28
 
28
29
  /**
@@ -48,6 +48,7 @@
48
48
 
49
49
  import { CLI } from '../name.mjs';
50
50
  import { LOADERS } from '../projection/loaders.mjs';
51
+ import { harnessHooks, harnessPayloads } from '../harness/records.mjs';
51
52
  import { renderOpencodePlugin } from './opencode.mjs';
52
53
  import { EVENTS } from './policy.mjs';
53
54
  import { AGENT_HOOK_SHIM } from './shim.mjs';
@@ -183,188 +184,93 @@ function patchFiles(text) {
183
184
  return files.filter((f) => f.path !== '');
184
185
  }
185
186
 
186
- const ADAPTERS = {
187
- claude: {
188
- id: 'claude',
189
- event: 'PreToolUse',
190
- matcher: 'Bash|Edit|Write|MultiEdit|NotebookEdit',
191
- denyExit: 2,
192
- verified: 'docs.claude.com/en/docs/claude-code/hooks, read 2026-10-16',
193
- root: (p) => p.cwd,
194
- extract: (p) => ({
195
- command: p.tool_name === 'Bash' ? (p.tool_input?.command ?? null) : null,
196
- // NotebookEdit spells its target `notebook_path`, not `file_path`: a key list
197
- // that misses it is a lease the notebook can walk straight through (review
198
- // 0003 Q16). Edit details travel with the path so the generated-region
199
- // check can tell an edit outside the region from one inside it (Q17).
200
- writes: ['Edit', 'Write', 'MultiEdit'].includes(p.tool_name)
201
- ? [writeOf(p.tool_input?.file_path, p.tool_input)].filter(Boolean)
202
- : p.tool_name === 'NotebookEdit'
203
- ? [writeOf(p.tool_input?.notebook_path, p.tool_input)].filter(Boolean)
204
- : [],
205
- }),
206
- deny: (message) => ({
207
- hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: message },
208
- }),
209
- // The delivery door. `additionalContext` is documented for this event and is
210
- // wrapped into the conversation at the point the hook fired, which is why the
211
- // packet goes here rather than into a system prompt: the cached prefix survives.
212
- prompt: {
213
- event: 'UserPromptSubmit',
214
- verified: 'code.claude.com/docs/en/hooks, read 2026-10-19',
215
- context: (text) => ({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: text } }),
216
- },
217
- post: {
218
- event: 'PostToolUse',
219
- matcher: 'Bash',
220
- verified: 'code.claude.com/docs/en/hooks, read 2026-10-19',
221
- context: (text) => ({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: text } }),
222
- },
223
- },
224
- codex: {
225
- id: 'codex',
226
- event: 'PreToolUse',
227
- matcher: 'Bash|apply_patch',
228
- denyExit: 2,
229
- verified: 'developers.openai.com/codex/hooks, read 2026-10-16',
230
- // Codex is the one harness that will not run a hook until a person has approved the
231
- // exact definition, keyed by its hash — so an install that does not say this leaves
232
- // the repository with a hook that never fires and no way to tell why.
233
- trust: 'Codex skips a hook nobody has trusted: review it with /hooks, or the entry stays inert.',
234
- root: (p) => p.cwd,
235
- extract: (p) => ({
236
- command: p.tool_name === 'Bash' ? (p.tool_input?.command ?? null) : null,
237
- // apply_patch carries its targets inside the patch text, one header line per file;
238
- // a move names its destination on the `+++` line after `*** Move File:`, and the
239
- // destination is the file the move writes (review 0003 Q16).
240
- writes:
241
- p.tool_name === 'apply_patch'
242
- ? patchWrites(String(p.tool_input?.command ?? ''))
243
- : [], // entries carry {path, patch} so the region check maps hunks (Q17)
244
- }),
245
- deny: (message) => ({
246
- hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: message },
247
- }),
248
- prompt: {
249
- event: 'UserPromptSubmit',
250
- verified: 'developers.openai.com/codex/hooks, read 2026-10-19',
251
- context: (text) => ({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: text } }),
252
- },
253
- post: {
254
- event: 'PostToolUse',
255
- matcher: 'Bash',
256
- verified: 'developers.openai.com/codex/hooks, read 2026-10-19',
257
- context: (text) => ({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: text } }),
258
- },
259
- },
260
- cursor: {
261
- id: 'cursor',
262
- event: 'preToolUse',
263
- matcher: 'Shell|Write|Edit',
264
- denyExit: 2,
265
- verified: 'cursor.com/docs/agent/hooks, read 2026-10-16',
266
- root: (p) => p.working_directory ?? p.cwd,
267
- extract: (p) => ({
268
- command: p.tool_name === 'Shell' ? (p.tool_input?.command ?? null) : null,
269
- writes: ['Write', 'Edit'].includes(p.tool_name)
270
- ? [writeOf(p.tool_input?.file_path ?? p.tool_input?.path, p.tool_input)].filter(Boolean)
271
- : [],
272
- }),
273
- deny: (message) => ({ permission: 'deny', user_message: message, agent_message: message }),
274
- // beforeSubmitPrompt is documented as continue/block and nothing else, so there is no
275
- // key to deliver a packet in. Writing the entry anyway would put a command that reads
276
- // a repository on the path of every prompt a person types, to say nothing.
277
- prompt: null,
278
- post: null,
279
- },
280
- antigravity: {
281
- id: 'antigravity',
282
- event: 'PreToolUse',
283
- matcher: 'run_command|write_to_file|replace_file_content|multi_replace_file_content',
284
- // The page documents the decision JSON and not an exit code, so the denial travels
285
- // in the JSON only: a non-zero exit whose meaning the vendor has not stated is not
286
- // a block, it is a guess that happens to be loud.
287
- denyExit: 0,
288
- verified: 'antigravity.google/docs/hooks, read 2026-10-16',
289
- root: (p) => p.workspacePaths?.[0] ?? p.cwd,
290
- extract: (p) => {
291
- const call = p.toolCall ?? {};
292
- const args = call.args ?? {};
293
- return {
294
- command: call.name === 'run_command' ? (args.CommandLine ?? null) : null,
295
- writes: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(call.name)
296
- ? [writeOf(args.TargetFile, args)].filter(Boolean)
297
- : [], // CodeContent/old/new travel with the path for the region check (Q17)
298
- };
299
- },
300
- deny: (message) => ({ decision: 'deny', reason: message }),
301
- // No prompt event is documented for this harness's hooks file, and a guessed event
302
- // name is a hook that never fires while its config says it does.
303
- prompt: null,
304
- post: null,
305
- },
306
- gemini: {
307
- id: 'gemini',
308
- event: 'BeforeTool',
309
- matcher: 'run_shell_command|write_file|replace',
310
- // The vendor's timeout unit is milliseconds (default 60000), not the seconds every
311
- // other row uses; a bare 10 here would be a ten-millisecond budget, which is a hook
312
- // that never finishes. The row carries its own value so the shared constant stays
313
- // honest for the four vendors that measure in seconds.
314
- timeout: 10000,
315
- // The reference documents the decision JSON and an exit-2 system block; the denial
316
- // travels in the JSON, as antigravity does.
317
- denyExit: 0,
318
- verified: 'geminicli.com/docs/hooks/reference/, read 2026-10-26',
319
- root: (p) => p.cwd,
320
- extract: (p) => ({
321
- command: p.tool_name === 'run_shell_command' ? (p.tool_input?.command ?? null) : null,
322
- writes: ['write_file', 'replace'].includes(p.tool_name)
323
- ? [writeOf(p.tool_input?.file_path, p.tool_input)].filter(Boolean)
187
+ // The payload a hook reads: where the tool name and its input sit, and the keys that
188
+ // name a command and a file. A record cannot carry a function, so the engine reads the
189
+ // record's keys and applies them: a harness that puts a field somewhere new is a new
190
+ // record, not a new branch here (Q71).
191
+ function pick(obj, keys) {
192
+ if (!obj || typeof obj !== 'object') return null;
193
+ for (const k of keys) {
194
+ const v = obj[k];
195
+ if (v !== undefined && v !== null) return v;
196
+ }
197
+ return null;
198
+ }
199
+
200
+ function extractOf(spec) {
201
+ return (p) => {
202
+ const payload = p ?? {};
203
+ // antigravity nests the call under `toolCall`; the rest name the tool at the top
204
+ // level. `toolNameKey` is present only on the nested shape, so its presence is
205
+ // the detector.
206
+ const nested = spec.toolNameKey !== undefined;
207
+ const call = nested ? (payload[spec.toolKey] ?? {}) : null;
208
+ const toolName = nested ? call[spec.toolNameKey] : payload[spec.toolKey];
209
+ const input = nested ? (call[spec.inputSubKey] ?? {}) : (payload[spec.inputKey] ?? {});
210
+ return {
211
+ command: (spec.shellTools ?? []).includes(toolName) ? (input[spec.commandKey] ?? null) : null,
212
+ // apply_patch carries its targets inside the patch text (writesFrom: "patch");
213
+ // every other write tool names its file through the path keys.
214
+ writes: (spec.writeTools ?? []).includes(toolName)
215
+ ? spec.writesFrom === 'patch'
216
+ ? patchWrites(String(input[spec.commandKey] ?? ''))
217
+ : [writeOf(pick(input, spec.pathOverrides?.[toolName] ?? spec.pathKeys), input)].filter(Boolean)
324
218
  : [],
325
- }),
326
- deny: (message) => ({ decision: 'deny', reason: message }),
327
- // BeforeAgent appends text to the prompt for this turn only — the same lifetime as a
328
- // brief. The documented output has no hookEventName field, unlike Claude's.
329
- prompt: {
330
- event: 'BeforeAgent',
331
- verified: 'geminicli.com/docs/hooks/reference/, read 2026-10-26',
332
- context: (text) => ({ hookSpecificOutput: { additionalContext: text } }),
333
- },
334
- // AfterTool appends to the tool result; its input carries tool_response, which is
335
- // what the lesson recall reads.
336
- post: {
337
- event: 'AfterTool',
338
- verified: 'geminicli.com/docs/hooks/reference/, read 2026-10-26',
339
- context: (text) => ({ hookSpecificOutput: { additionalContext: text } }),
340
- },
341
- },
342
- opencode: {
343
- id: 'opencode',
344
- event: 'execute.before',
345
- matcher: 'shell|write|edit',
346
- // The plugin is a file the tool owns whole, not a config entry to merge:
347
- // `file` routes `rendered` and `hookDocument` to its source (opencode.mjs).
348
- file: true,
349
- // A thrown error is the block; exit 2 is how the entry point signals it,
350
- // and the reason the plugin throws is what `deny` prints.
351
- denyExit: 2,
352
- verified: 'opencode.ai/v2/docs/build/plugins/server, read 2026-10-29',
353
- root: (p) => p.cwd,
354
- extract: (p) => ({
355
- command: p.tool === 'shell' ? (p.input?.command ?? null) : null,
356
- writes: ['write', 'edit'].includes(p.tool) ? [writeOf(p.input?.path, p.input)].filter(Boolean) : [],
357
- }),
358
- deny: (message) => ({ reason: message }),
359
- // session.hook('prompt') admits the prompt; the packet goes in front of it.
360
- prompt: {
361
- event: 'prompt',
362
- verified: 'opencode.ai/v2/docs/build/plugins/server, read 2026-10-29',
363
- context: (text) => ({ text }),
219
+ };
220
+ };
221
+ }
222
+
223
+ // The record's `rootKeys`: a list of accessor paths, first non-null wins. The caller
224
+ // applies its `?? process.cwd()` floor (factsFrom).
225
+ function rootOf(spec) {
226
+ return (p) => {
227
+ for (const path of spec.rootKeys ?? []) {
228
+ let v = p;
229
+ for (const seg of path) v = v?.[seg];
230
+ if (v !== undefined && v !== null) return v;
231
+ }
232
+ return null;
233
+ };
234
+ }
235
+
236
+ // The `__SIMT_TEXT__` sentinel in a record's deny and delivery templates: the text a
237
+ // decision or a delivery carries is substituted for it, and JSON.stringify downstream
238
+ // escapes whatever it carries.
239
+ const TEXT = '__SIMT_TEXT__';
240
+
241
+ function applyText(value, text) {
242
+ if (typeof value === 'string') return value === TEXT ? text : value;
243
+ if (Array.isArray(value)) return value.map((v) => applyText(v, text));
244
+ if (value && typeof value === 'object') {
245
+ const out = {};
246
+ for (const [k, v] of Object.entries(value)) out[k] = applyText(v, text);
247
+ return out;
248
+ }
249
+ return value;
250
+ }
251
+
252
+ // The hooks dialect per harness, as records (Q71): the data a config document and a
253
+ // decision are shaped by, with the page each shape was read from. What a record cannot
254
+ // carry is attached here — `root` and `extract` read the record's payload keys, and
255
+ // `deny`, `prompt.context` and `post.context` apply its templates. Module-internal on
256
+ // purpose: `adapters()` and `adapter()` are the only doors in, and both check the
257
+ // reader table, so an id with no hooks file is no adapter.
258
+ const HOOKS = harnessHooks();
259
+ const PAYLOADS = harnessPayloads();
260
+
261
+ const ADAPTERS = Object.fromEntries(
262
+ Object.entries(HOOKS).map(([id, h]) => [
263
+ id,
264
+ {
265
+ ...h,
266
+ root: rootOf(PAYLOADS[id]),
267
+ extract: extractOf(PAYLOADS[id]),
268
+ deny: (message) => applyText(h.deny, message),
269
+ ...(h.prompt ? { prompt: { ...h.prompt, context: (text) => applyText(h.prompt.delivery, text) } } : {}),
270
+ ...(h.post ? { post: { ...h.post, context: (text) => applyText(h.post.delivery, text) } } : {}),
364
271
  },
365
- post: null,
366
- },
367
- };
272
+ ]),
273
+ );
368
274
 
369
275
  /** The six adapters, in the order the reader table lists them. */
370
276
  export function adapters() {