@hassangameryt/freeflow 0.8.0 → 0.9.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 (290) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +3 -3
  5. package/.cursor-plugin/plugin.json +1 -1
  6. package/README.md +96 -149
  7. package/capabilities/cognitive-routing/SKILL.md +21 -129
  8. package/capabilities/cognitive-routing/references/coordinator.md +163 -0
  9. package/capabilities/cognitive-routing/references/worker.md +38 -0
  10. package/capabilities/compaction/SKILL.md +3 -3
  11. package/capabilities/compaction/references/summary-format.md +26 -15
  12. package/capabilities/messaging/SKILL.md +49 -0
  13. package/capabilities/task-memory/SKILL.md +87 -0
  14. package/command-surface.json +6 -54
  15. package/gemini-extension.json +1 -1
  16. package/hooks/claude/hooks.json +13 -3
  17. package/hooks/codex/hooks.json +2 -1
  18. package/hooks/shared/runtime-context.mjs +69 -77
  19. package/package.json +25 -8
  20. package/pi-extension/dist/ask-user/ask.js +108 -0
  21. package/pi-extension/dist/ask-user/form.js +250 -0
  22. package/pi-extension/dist/ask-user/questions.js +66 -0
  23. package/pi-extension/dist/ask-user/tool.js +124 -0
  24. package/pi-extension/dist/cognitive-routing/assembler.js +35 -11
  25. package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
  26. package/pi-extension/dist/cognitive-routing/config.js +3 -4
  27. package/pi-extension/dist/cognitive-routing/event-store.js +4 -1
  28. package/pi-extension/dist/cognitive-routing/gate.js +20 -5
  29. package/pi-extension/dist/cognitive-routing/handlers.js +15 -13
  30. package/pi-extension/dist/cognitive-routing/model-control.js +17 -8
  31. package/pi-extension/dist/cognitive-routing/projection.js +23 -6
  32. package/pi-extension/dist/cognitive-routing/provenance.js +3 -1
  33. package/pi-extension/dist/cognitive-routing/render.js +15 -15
  34. package/pi-extension/dist/cognitive-routing/runtime.js +4 -2
  35. package/pi-extension/dist/cognitive-routing/schemas.js +4 -4
  36. package/pi-extension/dist/cognitive-routing/session.js +3 -1
  37. package/pi-extension/dist/cognitive-routing/sources.js +29 -4
  38. package/pi-extension/dist/cognitive-routing/state.js +18 -8
  39. package/pi-extension/dist/cognitive-routing/status.js +4 -3
  40. package/pi-extension/dist/cognitive-routing/tools.js +29 -15
  41. package/pi-extension/dist/cognitive-routing/types.js +10 -1
  42. package/pi-extension/dist/compaction/carry.js +36 -22
  43. package/pi-extension/dist/compaction/config.js +4 -2
  44. package/pi-extension/dist/compaction/controller.js +52 -29
  45. package/pi-extension/dist/compaction/harness.js +19 -3
  46. package/pi-extension/dist/compaction/results.js +14 -13
  47. package/pi-extension/dist/compaction/tool.js +6 -6
  48. package/pi-extension/dist/host/catalog.js +20 -35
  49. package/pi-extension/dist/host/config-validation.js +233 -0
  50. package/pi-extension/dist/host/config.js +78 -91
  51. package/pi-extension/dist/host/inbound.js +142 -0
  52. package/pi-extension/dist/host/projection-tags.js +1 -1
  53. package/pi-extension/dist/host/prompts.js +24 -13
  54. package/pi-extension/dist/host/read-only-session.js +6 -1
  55. package/pi-extension/dist/host/request-history.js +7 -3
  56. package/pi-extension/dist/host/rows.js +25 -0
  57. package/pi-extension/dist/host/runtime-state.js +17 -2
  58. package/pi-extension/dist/host/section-keeper.js +85 -0
  59. package/pi-extension/dist/host/settings/freeflow-command.js +846 -93
  60. package/pi-extension/dist/host/settings/settings-view.js +27 -8
  61. package/pi-extension/dist/host/status.js +19 -5
  62. package/pi-extension/dist/index.js +638 -72
  63. package/pi-extension/dist/messaging/config.js +27 -0
  64. package/pi-extension/dist/messaging/messaging.js +398 -0
  65. package/pi-extension/dist/messaging/registry.js +57 -0
  66. package/pi-extension/dist/messaging/render.js +90 -0
  67. package/pi-extension/dist/messaging/tools.js +109 -0
  68. package/pi-extension/dist/messaging/transport.js +101 -0
  69. package/pi-extension/dist/provider-support/cache/keepalive.js +16 -4
  70. package/pi-extension/dist/task-memory/config.js +49 -0
  71. package/pi-extension/dist/task-memory/registry.js +119 -0
  72. package/pi-extension/dist/task-memory/render.js +107 -0
  73. package/pi-extension/dist/task-memory/runtime.js +994 -0
  74. package/pi-extension/dist/task-memory/tool.js +233 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/tool.js +1 -1
  76. package/pi-extension/dist/tool-execution/background.js +220 -28
  77. package/pi-extension/dist/tool-execution/config.js +14 -3
  78. package/pi-extension/dist/tool-execution/file-tracking.js +24 -4
  79. package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
  80. package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
  81. package/pi-extension/dist/tool-execution/tools.js +2 -2
  82. package/runtime/prompts/cognitive-routing.md +4 -4
  83. package/runtime/prompts/core.md +24 -14
  84. package/runtime/prompts/interaction-contract.md +7 -4
  85. package/runtime/prompts/tool-execution.md +3 -16
  86. package/runtime/prompts/working-method.md +16 -99
  87. package/skills/deliver-work/SKILL.md +76 -0
  88. package/skills/deliver-work/references/branch-closeout.md +134 -0
  89. package/skills/deliver-work/references/commit.md +83 -0
  90. package/skills/deliver-work/references/launch.md +94 -0
  91. package/skills/deliver-work/references/release.md +85 -0
  92. package/skills/deliver-work/scripts/commit-state.mjs +139 -0
  93. package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
  94. package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
  95. package/skills/deliver-work/scripts/push-route.mjs +122 -0
  96. package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
  97. package/skills/discuss-work/SKILL.md +148 -0
  98. package/skills/{discuss → discuss-work}/references/discussion-continuity.md +10 -27
  99. package/skills/discuss-work/references/learning-action.md +36 -0
  100. package/skills/discuss-work/scripts/choices.mjs +101 -0
  101. package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
  102. package/skills/evaluate-behavior/SKILL.md +107 -0
  103. package/skills/evaluate-behavior/references/authoring.md +381 -0
  104. package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
  105. package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
  106. package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
  107. package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
  108. package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
  109. package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
  110. package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
  111. package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
  112. package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
  113. package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
  114. package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
  115. package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
  116. package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
  117. package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
  118. package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
  119. package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
  120. package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
  121. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
  122. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
  123. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
  124. package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
  125. package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
  126. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
  127. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
  128. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
  129. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
  130. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
  131. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
  132. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
  133. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
  134. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
  135. package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
  136. package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
  137. package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
  138. package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
  139. package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
  140. package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
  141. package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
  142. package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
  143. package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
  144. package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
  145. package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
  146. package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
  147. package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
  148. package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
  149. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
  150. package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
  151. package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
  152. package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
  153. package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
  154. package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
  155. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +119 -14
  156. package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
  157. package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
  158. package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
  159. package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
  160. package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
  161. package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
  162. package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +235 -265
  163. package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
  164. package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
  165. package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
  166. package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
  167. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +159 -18
  168. package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-command-tool.mjs +21 -6
  169. package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
  170. package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
  171. package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
  172. package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
  173. package/skills/execute-work/SKILL.md +96 -104
  174. package/skills/execute-work/references/code-practices.md +27 -51
  175. package/skills/execute-work/references/design.md +271 -0
  176. package/skills/execute-work/references/diagnose.md +117 -0
  177. package/skills/execute-work/references/migration.md +108 -0
  178. package/skills/execute-work/references/simplify.md +65 -0
  179. package/skills/execute-work/scripts/self-review.mjs +422 -0
  180. package/skills/execute-work/scripts/touch-map.mjs +290 -0
  181. package/skills/review-work/SKILL.md +89 -65
  182. package/skills/review-work/references/adjudication.md +94 -0
  183. package/skills/review-work/references/artifact.md +198 -0
  184. package/skills/review-work/references/evals-and-judges.md +66 -0
  185. package/skills/review-work/references/work.md +177 -0
  186. package/skills/review-work/scripts/check-brief.mjs +128 -0
  187. package/skills/review-work/scripts/review-packet.mjs +233 -0
  188. package/skills/setup-freeflow/SKILL.md +68 -17
  189. package/skills/track-work/SKILL.md +52 -20
  190. package/skills/track-work/references/handoff.md +190 -0
  191. package/skills/track-work/references/projects.md +51 -0
  192. package/skills/track-work/references/working-record-format.md +36 -18
  193. package/skills/track-work/scripts/lib/document.mjs +17 -13
  194. package/skills/track-work/scripts/lib/format.mjs +2 -0
  195. package/skills/track-work/scripts/lib/recover.mjs +254 -0
  196. package/skills/track-work/scripts/lib/registry.mjs +656 -0
  197. package/skills/track-work/scripts/lib/search.mjs +40 -0
  198. package/skills/track-work/scripts/lib/store.mjs +46 -15
  199. package/skills/track-work/scripts/lib/views.mjs +50 -1
  200. package/skills/track-work/scripts/working-record.mjs +232 -13
  201. package/skills/verify-work/SKILL.md +45 -37
  202. package/skills/verify-work/references/feature-map.md +120 -0
  203. package/skills/verify-work/references/runtime-evidence.md +203 -0
  204. package/skills/verify-work/references/test-design.md +25 -6
  205. package/skills/verify-work/scripts/test-changes.mjs +246 -0
  206. package/skills/write-docs/SKILL.md +113 -25
  207. package/skills/write-docs/references/documentation-shapes.md +68 -3
  208. package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
  209. package/skills/write-docs/scripts/artifact-check.mjs +148 -0
  210. package/skills/write-docs/scripts/docs-check.mjs +96 -6
  211. package/skills/write-docs/scripts/plan-lint.mjs +123 -0
  212. package/skills/write-skill/SKILL.md +92 -52
  213. package/skills/write-skill/references/activation-and-wording.md +143 -0
  214. package/skills/write-skill/references/placement-and-dependencies.md +154 -0
  215. package/skills/write-skill/scripts/lib/compare.mjs +701 -0
  216. package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
  217. package/skills/write-skill/scripts/skill-author.mjs +51 -8
  218. package/capabilities/cognitive-routing/references/both-mode.md +0 -36
  219. package/capabilities/cognitive-routing/references/executor-mode.md +0 -48
  220. package/capabilities/cognitive-routing/references/helper-mode.md +0 -47
  221. package/plugin.json +0 -33
  222. package/skills/action-selection/SKILL.md +0 -92
  223. package/skills/action-selection/references/trajectory-stalls.md +0 -73
  224. package/skills/bypass/SKILL.md +0 -153
  225. package/skills/commit-work/SKILL.md +0 -93
  226. package/skills/commit-work/references/git-state-edges.md +0 -94
  227. package/skills/commit-work/references/simple-push.md +0 -94
  228. package/skills/commit-work/references/staging-decisions.md +0 -106
  229. package/skills/decision-gate/SKILL.md +0 -87
  230. package/skills/design-for-depth/SKILL.md +0 -108
  231. package/skills/design-for-depth/references/design-pressure-signals.md +0 -84
  232. package/skills/design-for-depth/references/implementation-decisions.md +0 -76
  233. package/skills/design-for-depth/references/interface-design-loop.md +0 -105
  234. package/skills/design-for-depth/references/module-and-dependency-design.md +0 -117
  235. package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -125
  236. package/skills/diagnose-failure/SKILL.md +0 -140
  237. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -50
  238. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
  239. package/skills/discuss/SKILL.md +0 -140
  240. package/skills/evaluate-skill/SKILL.md +0 -188
  241. package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -415
  242. package/skills/evaluate-skill/references/evaluation-design.md +0 -112
  243. package/skills/evaluate-skill/references/execution-and-evidence.md +0 -111
  244. package/skills/evaluate-skill/references/review-and-revision.md +0 -94
  245. package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -448
  246. package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
  247. package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
  248. package/skills/evaluate-skill/scripts/lib/review.mjs +0 -196
  249. package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -405
  250. package/skills/evaluate-skill/scripts/lib/trials.mjs +0 -103
  251. package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -181
  252. package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -182
  253. package/skills/execute-work/references/execute-work-edges.md +0 -78
  254. package/skills/finish-branch/SKILL.md +0 -117
  255. package/skills/finish-branch/references/integration-options.md +0 -80
  256. package/skills/handoff/SKILL.md +0 -82
  257. package/skills/handoff/references/handoff-templates.md +0 -150
  258. package/skills/launch-work/SKILL.md +0 -116
  259. package/skills/launch-work/references/launch-readiness.md +0 -85
  260. package/skills/migration-work/SKILL.md +0 -115
  261. package/skills/migration-work/references/migration-lifecycle.md +0 -82
  262. package/skills/release-work/SKILL.md +0 -118
  263. package/skills/release-work/references/release-evidence.md +0 -78
  264. package/skills/review-artifact/SKILL.md +0 -143
  265. package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
  266. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
  267. package/skills/review-work/references/adjudicate-work-review.md +0 -97
  268. package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
  269. package/skills/review-work/references/security-risk-lens.md +0 -67
  270. package/skills/setup-freeflow/references/activation-contract.md +0 -103
  271. package/skills/setup-freeflow/references/host-setup.md +0 -67
  272. package/skills/simplify-code/SKILL.md +0 -85
  273. package/skills/simplify-code/references/simplification-patterns.md +0 -70
  274. package/skills/verify-work/references/browser-runtime-evidence.md +0 -51
  275. package/skills/verify-work/references/integration-evidence.md +0 -49
  276. package/skills/verify-work/references/performance-evidence.md +0 -59
  277. package/skills/workflow/SKILL.md +0 -171
  278. package/skills/workflow/references/domain-skill-composition.md +0 -45
  279. package/skills/write-plan/SKILL.md +0 -92
  280. package/skills/write-plan/references/plan-shapes.md +0 -156
  281. package/skills/write-skill/references/activation-boundaries.md +0 -106
  282. package/skills/write-skill/references/agent-first-instructions.md +0 -133
  283. package/skills/write-skill/references/progressive-disclosure.md +0 -129
  284. package/skills/write-skill/references/skill-dependency-graphs.md +0 -192
  285. package/skills/write-spec/SKILL.md +0 -85
  286. package/skills/write-spec/references/artifact-standards.md +0 -86
  287. package/skills/write-spec/references/decision-records.md +0 -80
  288. package/skills/write-spec/references/spec-shapes.md +0 -143
  289. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
  290. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env node
2
+ // Puts the facts a self-review needs in one scratch report, so the agent reads its actual change rather than its
3
+ // memory of it: Git status, the diff stat against a base, every changed existing test, fixture, snapshot or shared
4
+ // test setup with its hunk (Verify Work's test-changes.mjs), each new test and whether it fails on the base, added lines
5
+ // that look like leftover debug output, per requirement the last recorded check and whether it ran after the last
6
+ // edit, and the feature files (Verify Work's feature map) that cover a changed file, each with whether it was driven
7
+ // live since that code last changed (Write Docs' docs-check --covering --driven). It lists; judging each item is the
8
+ // agent's and the user's.
9
+ //
10
+ // A check is recorded with `check`, which runs it and stamps it with the state it ran on: HEAD plus a hash of every
11
+ // uncommitted change, untracked files included. A requirement's check "ran after the last edit" when its stamp equals
12
+ // the current state's.
13
+ //
14
+ // Usage: self-review.mjs --base <ref> [--root <repository>] [--requirements <file>] [--test-command <command>]
15
+ // [--scratch <dir>] [--json]
16
+ // self-review.mjs check --id <R1> [--root <repository>] [--scratch <dir>] (--command <shell command> | -- <argv>)
17
+ // --requirements: a file whose lines start with a requirement ID (R1, - R2, ...), optionally ending in
18
+ // "check: <command>". --test-command runs each new test on the base in a temporary worktree; "{file}" in it is replaced
19
+ // by the test's path, otherwise the path is appended. --scratch defaults to a folder under the system temporary
20
+ // directory; pass the task's scratch/ when it has a Working Record. Exit code 0 when the report is written, 2 on a
21
+ // usage or Git error; `check` exits with the command's exit code.
22
+ import { execFileSync, spawnSync } from "node:child_process";
23
+ import { createHash } from "node:crypto";
24
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, appendFileSync, symlinkSync, writeFileSync } from "node:fs";
25
+ import { tmpdir } from "node:os";
26
+ import { basename, dirname, join, relative, resolve } from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+ import { parseArgs } from "node:util";
29
+
30
+ const USAGE = [
31
+ "Usage: self-review.mjs --base <ref> [--root <repository>] [--requirements <file>] [--test-command <command>] [--scratch <dir>] [--json]",
32
+ " self-review.mjs check --id <R1> [--root <repository>] [--scratch <dir>] (--command <shell command> | -- <argv>)",
33
+ ].join("\n");
34
+ const TEST_CHANGES = resolve(dirname(fileURLToPath(import.meta.url)), "../../verify-work/scripts/test-changes.mjs");
35
+ const DOCS_CHECK = resolve(dirname(fileURLToPath(import.meta.url)), "../../write-docs/scripts/docs-check.mjs");
36
+ const CAP = 40;
37
+ const LEFTOVER =
38
+ /\b(console\.(log|debug|trace)|debugger|print\(|println!|dbg!|fmt\.Print|var_dump|binding\.pry|breakpoint\(\)|pdb\.set_trace)|\b(TODO|FIXME|XXX)\b/;
39
+
40
+ function fail(message) {
41
+ console.error(message);
42
+ process.exit(2);
43
+ }
44
+
45
+ const argv = process.argv.slice(2);
46
+ const mode = argv[0] === "check" ? "check" : "report";
47
+ const dash = argv.indexOf("--");
48
+ const own = mode === "check" ? argv.slice(1, dash === -1 ? undefined : dash) : argv;
49
+ let values;
50
+ try {
51
+ ({ values } = parseArgs({
52
+ args: own,
53
+ options: {
54
+ base: { type: "string" },
55
+ root: { type: "string" },
56
+ requirements: { type: "string" },
57
+ "test-command": { type: "string" },
58
+ scratch: { type: "string" },
59
+ id: { type: "string" },
60
+ command: { type: "string" },
61
+ json: { type: "boolean", default: false },
62
+ },
63
+ }));
64
+ } catch (error) {
65
+ fail(`${error.message}\n${USAGE}`);
66
+ }
67
+
68
+ const git = (cwd, args, options = {}) =>
69
+ execFileSync("git", args, {
70
+ cwd,
71
+ encoding: "utf8",
72
+ maxBuffer: 256 * 1024 * 1024,
73
+ stdio: ["ignore", "pipe", "pipe"],
74
+ ...options,
75
+ });
76
+ const gitOrFail = (cwd, args, what) => {
77
+ try {
78
+ return git(cwd, args);
79
+ } catch (error) {
80
+ fail(`${what}: ${String(error.stderr || error.message).trim()}`);
81
+ }
82
+ };
83
+
84
+ const root = resolve(
85
+ gitOrFail(resolve(values.root ?? "."), ["rev-parse", "--show-toplevel"], "not inside a Git repository").trim(),
86
+ );
87
+ const scratch = resolve(
88
+ values.scratch ??
89
+ join(
90
+ tmpdir(),
91
+ "freeflow-self-review",
92
+ `${basename(root)}-${createHash("sha256").update(root).digest("hex").slice(0, 8)}`,
93
+ ),
94
+ );
95
+ mkdirSync(scratch, { recursive: true });
96
+ const logPath = join(scratch, "self-review-checks.jsonl");
97
+ const head = () => {
98
+ try {
99
+ return git(root, ["rev-parse", "--verify", "--quiet", "HEAD"]).trim();
100
+ } catch {
101
+ return null;
102
+ }
103
+ };
104
+
105
+ // The state a check observed: HEAD, every uncommitted change to tracked files, and every untracked file's content.
106
+ // Files under the scratch folder are left out, so recording a check does not change the state it recorded.
107
+ function stamp() {
108
+ const hash = createHash("sha256");
109
+ const commit = head();
110
+ hash.update(`${commit}\n`);
111
+ if (commit) hash.update(git(root, ["-c", "core.quotePath=false", "diff", "--binary", "--no-ext-diff", "HEAD"]));
112
+ const inScratch = (path) => !relative(scratch, join(root, path)).startsWith("..");
113
+ for (const path of git(root, ["ls-files", "-z", "--others", "--exclude-standard"]).split("\0").filter(Boolean)) {
114
+ if (inScratch(path)) continue;
115
+ hash.update(`\0${path}\0`);
116
+ try {
117
+ hash.update(readFileSync(join(root, path)));
118
+ } catch {
119
+ hash.update("unreadable");
120
+ }
121
+ }
122
+ return hash.digest("hex").slice(0, 16);
123
+ }
124
+
125
+ const readLog = () =>
126
+ existsSync(logPath)
127
+ ? readFileSync(logPath, "utf8")
128
+ .split("\n")
129
+ .filter(Boolean)
130
+ .flatMap((line) => {
131
+ try {
132
+ return [JSON.parse(line)];
133
+ } catch {
134
+ return [];
135
+ }
136
+ })
137
+ : [];
138
+
139
+ if (mode === "check") {
140
+ if (!values.id) fail(`check needs --id\n${USAGE}`);
141
+ const words = dash === -1 ? [] : argv.slice(dash + 1);
142
+ if (!values.command && !words.length) fail(`check needs --command or a command after --\n${USAGE}`);
143
+ const before = stamp();
144
+ const result = values.command
145
+ ? spawnSync(values.command, { cwd: root, shell: true, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 })
146
+ : spawnSync(words[0], words.slice(1), { cwd: root, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
147
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}${result.error ? String(result.error.message) : ""}`;
148
+ const exit = result.status ?? 1;
149
+ // A check that edits files (a formatter, a snapshot update) observed the state before its edits only.
150
+ const after = stamp();
151
+ const entry = {
152
+ id: values.id,
153
+ command: values.command ?? words.join(" "),
154
+ exit,
155
+ at: new Date().toISOString(),
156
+ head: head(),
157
+ stamp: before,
158
+ changedState: after !== before,
159
+ tail: output.trimEnd().split("\n").slice(-20),
160
+ };
161
+ appendFileSync(logPath, `${JSON.stringify(entry)}\n`);
162
+ process.stdout.write(output);
163
+ console.log(
164
+ `\n${values.id}: exit ${exit}; recorded in ${logPath}${entry.changedState ? " (the command changed files: rerun it to check the new state)" : ""}`,
165
+ );
166
+ process.exit(exit);
167
+ }
168
+
169
+ if (!values.base) fail(USAGE);
170
+ try {
171
+ git(root, ["rev-parse", "--verify", "--quiet", `${values.base}^{commit}`]);
172
+ } catch {
173
+ fail(`no such commit: ${values.base}`);
174
+ }
175
+ const baseCommit = git(root, ["rev-parse", `${values.base}^{commit}`]).trim();
176
+
177
+ const capped = (lines, cap = CAP) => ({ lines: lines.slice(0, cap), cut: Math.max(0, lines.length - cap) });
178
+ const status = capped(git(root, ["status", "--short", "--branch"]).trimEnd().split("\n").filter(Boolean));
179
+ const stat = git(root, ["-c", "core.quotePath=false", "diff", "--stat=120", "--no-color", "--no-ext-diff", baseCommit])
180
+ .trimEnd()
181
+ .split("\n")
182
+ .filter(Boolean);
183
+ const untracked = git(root, ["ls-files", "-z", "--others", "--exclude-standard"])
184
+ .split("\0")
185
+ .filter(Boolean)
186
+ .filter((path) => relative(scratch, join(root, path)).startsWith(".."));
187
+
188
+ // Feature files covering a changed path: documents with a "Driving it" section, the Write Docs feature file shape.
189
+ // A file is "stale" when its covered code changed after its Verified at, even with its text edited alongside.
190
+ const featureFiles = (() => {
191
+ const changed = [
192
+ ...git(root, ["-c", "core.quotePath=false", "diff", "--name-only", baseCommit]).split("\n").filter(Boolean),
193
+ ...untracked,
194
+ ];
195
+ if (!changed.length) return [];
196
+ const result = spawnSync(
197
+ process.execPath,
198
+ [DOCS_CHECK, ...changed.flatMap((path) => ["--covering", path]), "--driven", "--root", root, "--json"],
199
+ { cwd: root, encoding: "utf8" },
200
+ );
201
+ if (result.status !== 0) return [];
202
+ const found = new Map();
203
+ for (const { path, documents } of JSON.parse(result.stdout).paths)
204
+ for (const doc of documents) {
205
+ let text = "";
206
+ try {
207
+ text = readFileSync(join(root, doc.path), "utf8");
208
+ } catch {
209
+ continue;
210
+ }
211
+ if (!/^## Driving it\b/m.test(text)) continue;
212
+ const entry = found.get(doc.path) ?? { path: doc.path, verifiedAt: doc.verifiedAt, state: doc.state, covers: [] };
213
+ entry.covers.push(path);
214
+ found.set(doc.path, entry);
215
+ }
216
+ return [...found.values()];
217
+ })();
218
+
219
+ const testChanges = (extra) => {
220
+ const result = spawnSync(process.execPath, [TEST_CHANGES, "--base", baseCommit, "--root", root, "--json", ...extra], {
221
+ encoding: "utf8",
222
+ maxBuffer: 256 * 1024 * 1024,
223
+ });
224
+ if (result.status !== 0) fail(`test-changes failed: ${(result.stderr || result.stdout).trim()}`);
225
+ return JSON.parse(result.stdout);
226
+ };
227
+ const changedTests = testChanges([]).changes.map((change) => {
228
+ const hunk = change.hunks ? change.hunks.split("\n") : [];
229
+ return { ...change, hunks: hunk.slice(0, 80).join("\n"), hunkLinesCut: Math.max(0, hunk.length - 80) };
230
+ });
231
+
232
+ // New tests, each run on the base in a temporary worktree: a new test that passes there may not test the change.
233
+ const newTests = testChanges(["--new"]).tests.map((each) => ({ ...each, onBase: null }));
234
+ const testCommand = values["test-command"];
235
+ if (testCommand && newTests.some((each) => each.kind === "test")) {
236
+ const where = join(scratch, `base-${process.pid}-${Date.now()}`);
237
+ gitOrFail(root, ["worktree", "add", "--detach", "--quiet", where, baseCommit], "git worktree add failed");
238
+ try {
239
+ if (existsSync(join(root, "node_modules")) && !existsSync(join(where, "node_modules")))
240
+ symlinkSync(join(root, "node_modules"), join(where, "node_modules"), "dir");
241
+ // Every new file goes in, so a new test's new fixture is there too.
242
+ for (const each of newTests) {
243
+ mkdirSync(dirname(join(where, each.path)), { recursive: true });
244
+ copyFileSync(join(root, each.path), join(where, each.path));
245
+ }
246
+ for (const each of newTests.filter((test) => test.kind === "test").slice(0, 10)) {
247
+ const command = testCommand.includes("{file}")
248
+ ? testCommand.replaceAll("{file}", each.path)
249
+ : `${testCommand} ${each.path}`;
250
+ const run = spawnSync(command, { cwd: where, shell: true, encoding: "utf8", timeout: 300_000 });
251
+ const tail = `${run.stdout ?? ""}${run.stderr ?? ""}`.trimEnd().split("\n").slice(-5);
252
+ each.onBase = {
253
+ command,
254
+ exit: run.status,
255
+ result: run.status === 0 ? "passes on the base: it may not test the change" : "fails on the base",
256
+ tail,
257
+ };
258
+ }
259
+ } finally {
260
+ try {
261
+ git(root, ["worktree", "remove", "--force", where]);
262
+ } catch {
263
+ // Reported below: the agent removes it by hand.
264
+ }
265
+ }
266
+ if (existsSync(where)) newTests.push({ status: "note", path: where, kind: "leftover worktree: remove it" });
267
+ }
268
+
269
+ // Requirements and their last recorded checks.
270
+ const current = stamp();
271
+ const log = readLog();
272
+ let requirements = [];
273
+ if (values.requirements) {
274
+ const file = resolve(values.requirements);
275
+ if (!existsSync(file)) fail(`no such requirements file: ${values.requirements}`);
276
+ requirements = readFileSync(file, "utf8")
277
+ .split("\n")
278
+ .flatMap((line) => {
279
+ const match = /^\s*(?:[-*]\s+)?\**(R\d+)\**[:.)\]]?\s+(.*)$/.exec(line);
280
+ if (!match) return [];
281
+ const check = /\bcheck:\s*`?([^`]+?)`?\s*$/i.exec(match[2]);
282
+ const text = (check ? match[2].slice(0, check.index) : match[2]).replace(/[\s—–-]+$/, "").trim();
283
+ return [{ id: match[1], text, check: check?.[1] ?? null }];
284
+ })
285
+ .map((requirement) => {
286
+ const last = log.filter((entry) => entry.id === requirement.id).at(-1) ?? null;
287
+ const afterLastEdit = last ? last.stamp === current && !last.changedState : false;
288
+ return {
289
+ ...requirement,
290
+ lastCheck: last && { command: last.command, exit: last.exit, at: last.at, head: last.head },
291
+ afterLastEdit,
292
+ state: !last
293
+ ? "no check recorded"
294
+ : afterLastEdit
295
+ ? last.exit === 0
296
+ ? "passed on the current state"
297
+ : `failed on the current state (exit ${last.exit})`
298
+ : "stale: the change differs from the state it checked; rerun it",
299
+ };
300
+ });
301
+ }
302
+
303
+ // Added lines that look like leftover debug output or notes, in tracked files and untracked ones.
304
+ const leftovers = [];
305
+ let file = null;
306
+ let line = 0;
307
+ for (const text of git(root, [
308
+ "-c",
309
+ "core.quotePath=false",
310
+ "diff",
311
+ "--no-color",
312
+ "--no-ext-diff",
313
+ "--unified=0",
314
+ baseCommit,
315
+ ]).split("\n")) {
316
+ const header = /^\+\+\+ b\/(.*)$/.exec(text);
317
+ if (header) file = header[1];
318
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(text);
319
+ if (hunk) line = Number(hunk[1]);
320
+ else if (text.startsWith("+") && !text.startsWith("+++")) {
321
+ if (LEFTOVER.test(text)) leftovers.push({ at: `${file}:${line}`, text: text.slice(1).trim().slice(0, 160) });
322
+ line += 1;
323
+ }
324
+ }
325
+ for (const path of untracked) {
326
+ let contents;
327
+ try {
328
+ contents = readFileSync(join(root, path), "utf8");
329
+ } catch {
330
+ continue;
331
+ }
332
+ if (contents.includes("\0")) continue;
333
+ contents.split("\n").forEach((text, index) => {
334
+ if (LEFTOVER.test(text)) leftovers.push({ at: `${path}:${index + 1}`, text: text.trim().slice(0, 160) });
335
+ });
336
+ }
337
+
338
+ const report = {
339
+ command: "self-review",
340
+ root,
341
+ base: { ref: values.base, commit: baseCommit },
342
+ head: head(),
343
+ stamp: current,
344
+ status: status.lines,
345
+ statusCut: status.cut,
346
+ diffStat: stat,
347
+ untracked,
348
+ changedTests,
349
+ newTests,
350
+ requirements,
351
+ featureFiles,
352
+ leftovers: leftovers.slice(0, 20),
353
+ leftoversCut: Math.max(0, leftovers.length - 20),
354
+ notChecked: [
355
+ "whether each change is justified: judge every changed test and fixture with the user",
356
+ "callers of what changed: rerun the queries from Find what the change touches (touch-map.mjs)",
357
+ ...(testCommand ? [] : ["new tests on the base: pass --test-command to see each one fail there"]),
358
+ ...(values.requirements ? [] : ["requirements: pass --requirements with one line per requirement"]),
359
+ ],
360
+ };
361
+
362
+ const md = [`# Self-review: ${values.base} (${baseCommit.slice(0, 12)}) -> working tree`, ""];
363
+ md.push("## Status", "", "```text", ...status.lines, "```");
364
+ if (status.cut) md.push(`(${status.cut} more lines cut)`);
365
+ md.push("", "## Diff stat against the base", "", "```text", ...(stat.length ? stat : ["no tracked changes"]), "```");
366
+ if (untracked.length)
367
+ md.push(`Untracked: ${untracked.slice(0, CAP).join(", ")}${untracked.length > CAP ? ", ..." : ""}`);
368
+ md.push("", `## Changed existing tests, fixtures and setup (${changedTests.length})`, "");
369
+ if (!changedTests.length) md.push("None.");
370
+ for (const change of changedTests) {
371
+ md.push(
372
+ `### ${change.status} ${change.kind} ${change.to ? `${change.path} -> ${change.to}` : change.path} (+${change.added} -${change.removed})`,
373
+ );
374
+ if (change.hunks) md.push("", "```diff", change.hunks, "```");
375
+ if (change.hunkLinesCut) md.push(`(${change.hunkLinesCut} more diff lines cut)`);
376
+ md.push("");
377
+ }
378
+ md.push("", `## New tests (${newTests.length})`, "");
379
+ if (!newTests.length) md.push("None.");
380
+ for (const each of newTests)
381
+ md.push(
382
+ `- ${each.kind} ${each.path}${each.onBase ? `: ${each.onBase.result} (\`${each.onBase.command}\` exit ${each.onBase.exit})` : each.kind === "test" ? ": not run on the base" : ""}`,
383
+ );
384
+ md.push("", `## Requirements (${requirements.length})`, "");
385
+ if (!values.requirements) md.push("No requirements file given.");
386
+ for (const requirement of requirements) {
387
+ md.push(`- ${requirement.id} ${requirement.text} — ${requirement.state}`);
388
+ if (requirement.lastCheck)
389
+ md.push(
390
+ ` - last check: \`${requirement.lastCheck.command}\` exit ${requirement.lastCheck.exit} at ${requirement.lastCheck.at}`,
391
+ );
392
+ else if (requirement.check)
393
+ md.push(` - record one: self-review.mjs check --id ${requirement.id} --command "${requirement.check}"`);
394
+ }
395
+ md.push("", `## Feature files covering the change (${featureFiles.length})`, "");
396
+ if (!featureFiles.length) md.push("None found.");
397
+ else
398
+ md.push(
399
+ "A claim about what a user sees here is checked by driving these files' steps (Verify Work's Project Recipe). Stale: covered code changed since the file was last driven.",
400
+ "",
401
+ );
402
+ for (const each of featureFiles)
403
+ md.push(`- ${each.path}: ${each.state}, verified at ${each.verifiedAt ?? "none"} (covers ${each.covers.join(", ")})`);
404
+ md.push("", `## Possible leftovers (${leftovers.length})`, "");
405
+ if (!leftovers.length) md.push("None found.");
406
+ for (const each of report.leftovers) md.push(`- ${each.at}: ${each.text}`);
407
+ if (report.leftoversCut) md.push(`(${report.leftoversCut} more cut)`);
408
+ md.push("", "## Not checked by this report", "", ...report.notChecked.map((item) => `- ${item}`), "");
409
+ const reportPath = join(scratch, "self-review.md");
410
+ writeFileSync(reportPath, md.join("\n"));
411
+ report.reportPath = reportPath;
412
+
413
+ if (values.json) console.log(JSON.stringify(report, null, 2));
414
+ else {
415
+ const stale = requirements.filter((each) => !each.afterLastEdit).length;
416
+ const passingOnBase = newTests.filter((each) => each.onBase?.exit === 0).length;
417
+ console.log(`Self-review report: ${reportPath}`);
418
+ console.log(
419
+ `${stat.length ? stat.at(-1).trim() : "no tracked changes"}; ${untracked.length} untracked; ${changedTests.length} changed existing test file(s); ${newTests.length} new; ${passingOnBase} new test(s) pass on the base; ${requirements.length} requirement(s), ${stale} without a check after the last edit; ${featureFiles.length} feature file(s) cover the change, ${featureFiles.filter((each) => each.state !== "current").length} not driven since; ${leftovers.length} possible leftover(s).`,
420
+ );
421
+ console.log("Read the report before you report the change.");
422
+ }