@aperant/framework 0.10.0 → 0.12.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 (199) hide show
  1. package/CHANGELOG.md +236 -1
  2. package/agents/apt-planner.md +15 -0
  3. package/agents/apt-verifier.md +15 -10
  4. package/bin/apt-proof-video.mjs +256 -76
  5. package/dist/cli/commands/apply-recipe.d.mts +6 -0
  6. package/dist/cli/commands/apply-recipe.d.mts.map +1 -0
  7. package/dist/cli/commands/apply-recipe.mjs +98 -0
  8. package/dist/cli/commands/apply-recipe.mjs.map +1 -0
  9. package/dist/cli/commands/catch-up.d.mts +9 -0
  10. package/dist/cli/commands/catch-up.d.mts.map +1 -0
  11. package/dist/cli/commands/catch-up.mjs +64 -0
  12. package/dist/cli/commands/catch-up.mjs.map +1 -0
  13. package/dist/cli/commands/mobile-prepare.d.mts +53 -0
  14. package/dist/cli/commands/mobile-prepare.d.mts.map +1 -0
  15. package/dist/cli/commands/mobile-prepare.mjs +139 -0
  16. package/dist/cli/commands/mobile-prepare.mjs.map +1 -0
  17. package/dist/cli/commands/pr-review.d.mts +16 -4
  18. package/dist/cli/commands/pr-review.d.mts.map +1 -1
  19. package/dist/cli/commands/pr-review.mjs +30 -11
  20. package/dist/cli/commands/pr-review.mjs.map +1 -1
  21. package/dist/cli/commands/recipe-diff.d.mts +6 -0
  22. package/dist/cli/commands/recipe-diff.d.mts.map +1 -0
  23. package/dist/cli/commands/recipe-diff.mjs +75 -0
  24. package/dist/cli/commands/recipe-diff.mjs.map +1 -0
  25. package/dist/cli/commands/roadmap.d.mts.map +1 -1
  26. package/dist/cli/commands/roadmap.mjs +53 -0
  27. package/dist/cli/commands/roadmap.mjs.map +1 -1
  28. package/dist/cli/commands/route.d.mts.map +1 -1
  29. package/dist/cli/commands/route.mjs +92 -2
  30. package/dist/cli/commands/route.mjs.map +1 -1
  31. package/dist/cli/commands/verify-sandbox.d.mts +6 -0
  32. package/dist/cli/commands/verify-sandbox.d.mts.map +1 -0
  33. package/dist/cli/commands/verify-sandbox.mjs +200 -0
  34. package/dist/cli/commands/verify-sandbox.mjs.map +1 -0
  35. package/dist/cli/config/share-policy.d.mts.map +1 -1
  36. package/dist/cli/config/share-policy.mjs +1 -0
  37. package/dist/cli/config/share-policy.mjs.map +1 -1
  38. package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
  39. package/dist/cli/config/upgrade-gitignore.mjs +1 -0
  40. package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
  41. package/dist/cli/consistency/parse-qa.d.mts.map +1 -1
  42. package/dist/cli/consistency/parse-qa.mjs +18 -4
  43. package/dist/cli/consistency/parse-qa.mjs.map +1 -1
  44. package/dist/cli/consistency/rules/r5-verdict-consistency.d.mts.map +1 -1
  45. package/dist/cli/consistency/rules/r5-verdict-consistency.mjs +11 -10
  46. package/dist/cli/consistency/rules/r5-verdict-consistency.mjs.map +1 -1
  47. package/dist/cli/coordination/catch-up.d.mts +26 -0
  48. package/dist/cli/coordination/catch-up.d.mts.map +1 -0
  49. package/dist/cli/coordination/catch-up.mjs +239 -0
  50. package/dist/cli/coordination/catch-up.mjs.map +1 -0
  51. package/dist/cli/coordination/last-seen.d.mts +45 -0
  52. package/dist/cli/coordination/last-seen.d.mts.map +1 -0
  53. package/dist/cli/coordination/last-seen.mjs +128 -0
  54. package/dist/cli/coordination/last-seen.mjs.map +1 -0
  55. package/dist/cli/coordination/store.d.mts +15 -0
  56. package/dist/cli/coordination/store.d.mts.map +1 -1
  57. package/dist/cli/coordination/store.mjs +16 -0
  58. package/dist/cli/coordination/store.mjs.map +1 -1
  59. package/dist/cli/coverage-check/user-outcomes.d.mts +7 -2
  60. package/dist/cli/coverage-check/user-outcomes.d.mts.map +1 -1
  61. package/dist/cli/coverage-check/user-outcomes.mjs +50 -17
  62. package/dist/cli/coverage-check/user-outcomes.mjs.map +1 -1
  63. package/dist/cli/dispatch.d.mts.map +1 -1
  64. package/dist/cli/dispatch.mjs +30 -0
  65. package/dist/cli/dispatch.mjs.map +1 -1
  66. package/dist/cli/gate/gates/verify-approved.d.mts +0 -48
  67. package/dist/cli/gate/gates/verify-approved.d.mts.map +1 -1
  68. package/dist/cli/gate/gates/verify-approved.mjs +23 -80
  69. package/dist/cli/gate/gates/verify-approved.mjs.map +1 -1
  70. package/dist/cli/git/default-branch.d.mts +51 -0
  71. package/dist/cli/git/default-branch.d.mts.map +1 -0
  72. package/dist/cli/git/default-branch.mjs +234 -0
  73. package/dist/cli/git/default-branch.mjs.map +1 -0
  74. package/dist/cli/git/identity.d.mts +3 -5
  75. package/dist/cli/git/identity.d.mts.map +1 -1
  76. package/dist/cli/git/identity.mjs +10 -4
  77. package/dist/cli/git/identity.mjs.map +1 -1
  78. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  79. package/dist/cli/install/legacy-paths.mjs +2 -0
  80. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  81. package/dist/cli/install/mcp-server-specs.d.mts +6 -5
  82. package/dist/cli/install/mcp-server-specs.d.mts.map +1 -1
  83. package/dist/cli/install/mcp-server-specs.mjs +22 -4
  84. package/dist/cli/install/mcp-server-specs.mjs.map +1 -1
  85. package/dist/cli/roadmap/backend-adapter.d.mts +43 -0
  86. package/dist/cli/roadmap/backend-adapter.d.mts.map +1 -0
  87. package/dist/cli/roadmap/backend-adapter.mjs +222 -0
  88. package/dist/cli/roadmap/backend-adapter.mjs.map +1 -0
  89. package/dist/cli/util/runtime-capabilities.d.mts +7 -0
  90. package/dist/cli/util/runtime-capabilities.d.mts.map +1 -1
  91. package/dist/cli/util/runtime-capabilities.mjs +65 -0
  92. package/dist/cli/util/runtime-capabilities.mjs.map +1 -1
  93. package/dist/cli/verify-proof/manifest-validator.mjs +13 -0
  94. package/dist/cli/verify-proof/manifest-validator.mjs.map +1 -1
  95. package/dist/cli/verify-proof/resolver.d.mts +10 -7
  96. package/dist/cli/verify-proof/resolver.d.mts.map +1 -1
  97. package/dist/cli/verify-proof/resolver.mjs +28 -7
  98. package/dist/cli/verify-proof/resolver.mjs.map +1 -1
  99. package/dist/cli/verify-proof/sandbox/apply.d.mts +64 -0
  100. package/dist/cli/verify-proof/sandbox/apply.d.mts.map +1 -0
  101. package/dist/cli/verify-proof/sandbox/apply.mjs +379 -0
  102. package/dist/cli/verify-proof/sandbox/apply.mjs.map +1 -0
  103. package/dist/cli/verify-proof/sandbox/gate-predicate.d.mts +63 -0
  104. package/dist/cli/verify-proof/sandbox/gate-predicate.d.mts.map +1 -0
  105. package/dist/cli/verify-proof/sandbox/gate-predicate.mjs +115 -0
  106. package/dist/cli/verify-proof/sandbox/gate-predicate.mjs.map +1 -0
  107. package/dist/cli/verify-proof/sandbox/pattern-matcher.d.mts +57 -0
  108. package/dist/cli/verify-proof/sandbox/pattern-matcher.d.mts.map +1 -0
  109. package/dist/cli/verify-proof/sandbox/pattern-matcher.mjs +124 -0
  110. package/dist/cli/verify-proof/sandbox/pattern-matcher.mjs.map +1 -0
  111. package/dist/cli/verify-proof/sandbox/recipe-diff.d.mts +28 -0
  112. package/dist/cli/verify-proof/sandbox/recipe-diff.d.mts.map +1 -0
  113. package/dist/cli/verify-proof/sandbox/recipe-diff.mjs +190 -0
  114. package/dist/cli/verify-proof/sandbox/recipe-diff.mjs.map +1 -0
  115. package/dist/cli/verify-proof/sandbox/sandbox-config.d.mts +25 -0
  116. package/dist/cli/verify-proof/sandbox/sandbox-config.d.mts.map +1 -0
  117. package/dist/cli/verify-proof/sandbox/sandbox-config.mjs +85 -0
  118. package/dist/cli/verify-proof/sandbox/sandbox-config.mjs.map +1 -0
  119. package/dist/cli/verify-proof/sandbox/self-test.d.mts +39 -0
  120. package/dist/cli/verify-proof/sandbox/self-test.d.mts.map +1 -0
  121. package/dist/cli/verify-proof/sandbox/self-test.mjs +248 -0
  122. package/dist/cli/verify-proof/sandbox/self-test.mjs.map +1 -0
  123. package/dist/plugin/.claude-plugin/plugin.json +2 -1
  124. package/dist/plugin/agents/apt-planner.md +15 -0
  125. package/dist/plugin/agents/apt-verifier.md +15 -10
  126. package/dist/plugin/skills/apt/SKILL.md +29 -0
  127. package/dist/plugin/skills/apt-catch-up/SKILL.md +79 -0
  128. package/dist/plugin/skills/apt-plan/SKILL.md +23 -0
  129. package/dist/plugin/skills/apt-pr-review/SKILL.md +5 -4
  130. package/dist/plugin/skills/apt-run/SKILL.md +1 -1
  131. package/dist/plugin/skills/apt-setup/SKILL.md +132 -0
  132. package/dist/plugin/skills/apt-verify/SKILL.md +20 -15
  133. package/dist/plugin/skills/apt-verify-proof/SKILL.md +146 -11
  134. package/dist/proof-report.d.ts.map +1 -1
  135. package/dist/proof-report.js +4 -1
  136. package/dist/proof-report.js.map +1 -1
  137. package/dist/types/config.d.ts +42 -4
  138. package/dist/types/config.d.ts.map +1 -1
  139. package/dist/types/qa-scoring.d.ts +45 -9
  140. package/dist/types/qa-scoring.d.ts.map +1 -1
  141. package/dist/types/qa-scoring.js +34 -13
  142. package/dist/types/qa-scoring.js.map +1 -1
  143. package/dist/types/state.d.ts +7 -2
  144. package/dist/types/state.d.ts.map +1 -1
  145. package/dist/types/task-record.d.ts +10 -2
  146. package/dist/types/task-record.d.ts.map +1 -1
  147. package/drivers/mobile/README.md +40 -0
  148. package/drivers/mobile/driver.mjs +106 -0
  149. package/drivers/mobile/manifest.json +49 -0
  150. package/package.json +1 -1
  151. package/prompts/inbox-clarification.md +11 -0
  152. package/prompts/inbox-triage.md +20 -0
  153. package/prompts/qa_orchestrator_agentic.md +18 -17
  154. package/prompts/qa_reviewer.md +17 -15
  155. package/skills/apt/SKILL.md +29 -0
  156. package/skills/apt-catch-up/SKILL.md +79 -0
  157. package/skills/apt-plan/SKILL.md +23 -0
  158. package/skills/apt-pr-review/SKILL.md +5 -4
  159. package/skills/apt-run/SKILL.md +1 -1
  160. package/skills/apt-setup/SKILL.md +132 -0
  161. package/skills/apt-verifier.md +7 -4
  162. package/skills/apt-verify/SKILL.md +20 -15
  163. package/skills/apt-verify-proof/SKILL.md +146 -11
  164. package/src/cli/commands/apply-recipe.mjs +105 -0
  165. package/src/cli/commands/catch-up.mjs +67 -0
  166. package/src/cli/commands/mobile-prepare.mjs +151 -0
  167. package/src/cli/commands/pr-review.mjs +32 -11
  168. package/src/cli/commands/recipe-diff.mjs +81 -0
  169. package/src/cli/commands/roadmap.mjs +54 -0
  170. package/src/cli/commands/route.mjs +92 -1
  171. package/src/cli/commands/verify-sandbox.mjs +231 -0
  172. package/src/cli/config/share-policy.mjs +1 -0
  173. package/src/cli/config/upgrade-gitignore.mjs +1 -0
  174. package/src/cli/consistency/parse-qa.mjs +20 -4
  175. package/src/cli/consistency/rules/r5-verdict-consistency.mjs +11 -13
  176. package/src/cli/coordination/catch-up.mjs +231 -0
  177. package/src/cli/coordination/last-seen.mjs +131 -0
  178. package/src/cli/coordination/store.mjs +18 -0
  179. package/src/cli/coverage-check/user-outcomes.mjs +52 -17
  180. package/src/cli/dispatch.mjs +29 -0
  181. package/src/cli/gate/gates/verify-approved.mjs +22 -81
  182. package/src/cli/git/default-branch.mjs +250 -0
  183. package/src/cli/git/identity.mjs +9 -3
  184. package/src/cli/install/legacy-paths.mjs +2 -0
  185. package/src/cli/install/mcp-server-specs.mjs +24 -4
  186. package/src/cli/roadmap/backend-adapter.mjs +231 -0
  187. package/src/cli/util/runtime-capabilities.mjs +67 -0
  188. package/src/cli/verify-proof/manifest-validator.mjs +15 -0
  189. package/src/cli/verify-proof/resolver.mjs +27 -7
  190. package/src/cli/verify-proof/sandbox/apply.mjs +401 -0
  191. package/src/cli/verify-proof/sandbox/gate-predicate.mjs +126 -0
  192. package/src/cli/verify-proof/sandbox/pattern-matcher.mjs +127 -0
  193. package/src/cli/verify-proof/sandbox/recipe-diff.mjs +208 -0
  194. package/src/cli/verify-proof/sandbox/sandbox-config.mjs +82 -0
  195. package/src/cli/verify-proof/sandbox/sandbox-schema.json +33 -0
  196. package/src/cli/verify-proof/sandbox/self-test.mjs +265 -0
  197. package/templates/config.json +21 -1
  198. package/templates/proof-verification.md +27 -8
  199. package/workflows/verify-proof.md +376 -302
@@ -32,7 +32,12 @@ import {
32
32
  getTaskIsolationConfig,
33
33
  getTrustedSkillSources,
34
34
  loadMergedProjectConfig,
35
+ loadProjectConfig,
35
36
  } from '../config/load.mjs'
37
+ import { resolveSharePolicy } from '../config/share-policy.mjs'
38
+ import { computeCatchUpPanel } from '../coordination/catch-up.mjs'
39
+ import { createCoordinationStore } from '../coordination/store.mjs'
40
+ import { resolveLocalDefaultBranch } from '../git/default-branch.mjs'
36
41
  import '../gate/gates/index.mjs'
37
42
  import * as gateRegistry from '../gate/registry.mjs'
38
43
  import { detectHost, SUPPORTED_HOSTS } from '../host/detect.mjs'
@@ -96,6 +101,78 @@ function readUpdateCheckCache() {
96
101
  return null
97
102
  }
98
103
 
104
+ /**
105
+ * Is this an interactive (TTY) apt call? The catch-up panel + Last-Seen Marker
106
+ * are gated to interactive contexts (LD-02) — exactly like the statusline
107
+ * isTTY gate — so spawned subagents, fan-out workers, and non-TTY/piped calls
108
+ * are strict no-ops (they neither read nor write the marker). `APT_FORCE_TTY`
109
+ * forces the gate open for tests; `APT_CATCH_UP_DISABLE` forces it closed so a
110
+ * background spawner can opt every nested call out.
111
+ */
112
+ function isInteractiveTty() {
113
+ if (process.env.APT_CATCH_UP_DISABLE === '1') return false
114
+ if (process.env.APT_FORCE_TTY === '1') return true
115
+ return Boolean(process.stdout?.isTTY || process.stderr?.isTTY)
116
+ }
117
+
118
+ /**
119
+ * Lazy-compute the catch-up panel for an interactive call and advance the
120
+ * Last-Seen Marker AFTER the panel is evaluated (R2/LD-02). Returns the panel
121
+ * (or null) to fold onto the route envelope as a top-level `catch_up` field.
122
+ *
123
+ * Marker-advance ordering (correctness-critical):
124
+ * - last_run_at = now — always (engagement, not greeting);
125
+ * - last_main_sha = defaultBranch.sha — only AFTER the digest was computed,
126
+ * i.e. only when we resolved a branch
127
+ * (even when the digest is empty);
128
+ * - last_active_task = welcome-back task id, when present.
129
+ *
130
+ * Non-TTY callers never reach this function (the caller gates on
131
+ * isInteractiveTty), so background/spawned calls never touch the marker.
132
+ *
133
+ * @param {string} targetDir
134
+ * @returns {object|null} the catch_up panel, or null.
135
+ */
136
+ function computeCatchUpForRoute(targetDir) {
137
+ // Setup mirrors cmdCatchUp in commands/catch-up.mjs (recompute-only variant).
138
+ const config = loadProjectConfig(targetDir)
139
+ if (!config) return null
140
+
141
+ const store = createCoordinationStore(targetDir)
142
+ const marker = store.readLastSeen()
143
+ const td = config?.collaboration?.team_digest
144
+ const vis = resolveSharePolicy(config).visibility
145
+ const needsDigest = td?.enabled !== false && (vis === 'team' || vis === 'oss')
146
+ const defaultBranch = needsDigest ? resolveLocalDefaultBranch(targetDir) : null
147
+ const now = Date.now()
148
+
149
+ let panel = null
150
+ try {
151
+ panel = computeCatchUpPanel(targetDir, {
152
+ config,
153
+ now,
154
+ marker,
155
+ defaultBranch,
156
+ })
157
+ } catch (e) {
158
+ console.warn('[apt] catch-up:', e?.message ?? e)
159
+ panel = null
160
+ }
161
+
162
+ // Advance the marker AFTER panel eval. last_main_sha only when we actually
163
+ // resolved a default branch (the digest was computed, possibly empty).
164
+ const advance = { last_run_at: new Date(now).toISOString() }
165
+ if (defaultBranch?.sha) advance.last_main_sha = defaultBranch.sha
166
+ if (panel?.welcome_back?.task_id) advance.last_active_task = panel.welcome_back.task_id
167
+ try {
168
+ store.advanceLastSeen(advance)
169
+ } catch {
170
+ /* best-effort — never block routing on a marker write */
171
+ }
172
+
173
+ return panel
174
+ }
175
+
99
176
  /**
100
177
  * Deterministic mapping from an explicit track flag (--quick/--deep/--debug)
101
178
  * to its canonical {track, primary_skill} pair. Flag overrides are user
@@ -309,6 +386,13 @@ export function cmdRoute(projectDir, extraArgs) {
309
386
  }
310
387
  }
311
388
 
389
+ // Catch-up panel (LD-11): lazy-compute on interactive (TTY) calls only, and
390
+ // only when config exists (init has no config yet). The compute also
391
+ // advances the Last-Seen Marker after panel eval (LD-02). Non-TTY calls are
392
+ // strict no-ops — no read, no write, no field. Folded onto every returned
393
+ // envelope below as a top-level `catch_up` field, mirroring update_check.
394
+ const catch_up = hasConfig && isInteractiveTty() ? computeCatchUpForRoute(targetDir) : null
395
+
312
396
  // ---------------- Deterministic branches (zero-behavior-change) ----------------
313
397
  // These fire regardless of host detection status — missing host is not
314
398
  // fatal for init/status/help/skill-passthrough.
@@ -322,6 +406,7 @@ export function cmdRoute(projectDir, extraArgs) {
322
406
  host_capabilities,
323
407
  task_isolation,
324
408
  update_check,
409
+ catch_up: null,
325
410
  reason: 'No .aperant/config.json found — first-time setup required',
326
411
  })
327
412
  }
@@ -335,6 +420,7 @@ export function cmdRoute(projectDir, extraArgs) {
335
420
  host_capabilities,
336
421
  task_isolation,
337
422
  update_check,
423
+ catch_up,
338
424
  reason: 'No input provided — showing project status',
339
425
  })
340
426
  }
@@ -348,6 +434,7 @@ export function cmdRoute(projectDir, extraArgs) {
348
434
  host_capabilities,
349
435
  task_isolation,
350
436
  update_check,
437
+ catch_up,
351
438
  reason: 'Help requested',
352
439
  })
353
440
  }
@@ -395,6 +482,7 @@ export function cmdRoute(projectDir, extraArgs) {
395
482
  host_capabilities,
396
483
  task_isolation,
397
484
  update_check,
485
+ catch_up,
398
486
  ...(gate_preview ? { gate_preview } : {}),
399
487
  reason: `Direct skill invocation: apt:${firstWord}`,
400
488
  })
@@ -444,6 +532,7 @@ export function cmdRoute(projectDir, extraArgs) {
444
532
  host_capabilities,
445
533
  task_isolation,
446
534
  update_check,
535
+ catch_up,
447
536
  ...(gate_preview ? { gate_preview } : {}),
448
537
  ...(spar_inject_gates ? { spar_inject_gates } : {}),
449
538
  classification: {
@@ -523,7 +612,9 @@ export function cmdRoute(projectDir, extraArgs) {
523
612
  )
524
613
  }
525
614
 
526
- return ok(envelope)
615
+ // Fold the catch-up panel onto the classify envelope as a top-level field,
616
+ // mirroring update_check (LD-11). null when non-TTY / neither half fires.
617
+ return ok({ ...envelope, catch_up })
527
618
  }
528
619
 
529
620
  /**
@@ -0,0 +1,231 @@
1
+ /**
2
+ * commands/verify-sandbox.mjs — `apt-tools verify-sandbox <projectDir>`.
3
+ *
4
+ * Drives runSelfTest() then atomically writes
5
+ * .aperant/state/verify-proof-sandbox-verified.json with the v2-shaped
6
+ * verdict envelope. On win32, short-circuits to a platform_unsupported
7
+ * verdict WITHOUT spawning child processes (ID-06 v1 supported floor =
8
+ * darwin + linux).
9
+ *
10
+ * The state file is written for BOTH verified-true and verified-false
11
+ * outcomes (ID-02 — no silent state mutation). The sandbox gate at
12
+ * apt-verify-proof/SKILL.md Step 2c (subtask 5.1) reads this file.
13
+ */
14
+
15
+ import { join, resolve } from 'node:path'
16
+ import { atomicWriteJson } from '../util/atomic-write.mjs'
17
+ import { withFileLock } from '../util/fs-lock.mjs'
18
+ import { err, exitWith, ok } from '../util/result.mjs'
19
+ import { loadRuntimeCapabilities } from '../util/runtime-capabilities.mjs'
20
+ import { matchPatternForProject } from '../verify-proof/sandbox/pattern-matcher.mjs'
21
+ import {
22
+ discoverBundledRecipes,
23
+ readSandboxConfig,
24
+ } from '../verify-proof/sandbox/sandbox-config.mjs'
25
+ import { runSelfTest } from '../verify-proof/sandbox/self-test.mjs'
26
+
27
+ /**
28
+ * @param {string} projectDir
29
+ * @param {string[]} [extra]
30
+ */
31
+ export async function cmdVerifySandbox(projectDir, _extra = []) {
32
+ if (!projectDir) return err('Usage: apt-tools verify-sandbox <project-dir>')
33
+
34
+ const target = resolve(projectDir)
35
+ const platform = process.platform
36
+ const statePath = join(target, '.aperant', 'state', 'verify-proof-sandbox-verified.json')
37
+ const at = new Date().toISOString()
38
+
39
+ const host = {
40
+ platform,
41
+ arch: process.arch,
42
+ nodeVersion: process.version,
43
+ }
44
+
45
+ // Resolve recipeId from config or runtime detection.
46
+ const sandboxCfg = readSandboxConfig(target)
47
+ const recipes = discoverBundledRecipes()
48
+ let recipeId = sandboxCfg?.recipeId ?? null
49
+ let recipeVersion = sandboxCfg?.recipeVersion ?? null
50
+ if (!recipeId) {
51
+ const runtime = loadRuntimeCapabilities(target)
52
+ const match = matchPatternForProject(target, runtime, recipes)
53
+ recipeId = match.recipeId
54
+ }
55
+ // Always populate recipeVersion from bundled recipes when absent — covers
56
+ // the case where config pins recipeId but omits recipeVersion (LOG-003).
57
+ if (recipeId && !recipeVersion) {
58
+ recipeVersion = recipes.find((r) => r.recipeId === recipeId)?.recipeVersion ?? null
59
+ }
60
+
61
+ // win32 short-circuit (AC12, ID-06 v1 floor).
62
+ if (platform === 'win32') {
63
+ const envelope = {
64
+ verified: false,
65
+ verdict: 'needs_human',
66
+ reason: 'platform_unsupported',
67
+ recipeId,
68
+ recipeVersion,
69
+ verifiedAt: at,
70
+ host,
71
+ }
72
+ const writeErr = writeStateFile(statePath, envelope)
73
+ if (writeErr) {
74
+ return exitWith(
75
+ {
76
+ status: 'error',
77
+ command: 'verify-sandbox',
78
+ code: 'E_STATE_WRITE_FAILED',
79
+ verdict: 'needs_human',
80
+ reason: 'state_write_failed',
81
+ cause: writeErr.message,
82
+ project_dir: target,
83
+ state_path: statePath,
84
+ },
85
+ 1,
86
+ )
87
+ }
88
+ return ok({
89
+ status: 'ok',
90
+ command: 'verify-sandbox',
91
+ project_dir: target,
92
+ state_path: statePath,
93
+ ...envelope,
94
+ })
95
+ }
96
+
97
+ if (!recipeId) {
98
+ const envelope = {
99
+ verified: false,
100
+ verdict: 'needs_human',
101
+ reason: 'no_recipe_available',
102
+ recipeId: null,
103
+ recipeVersion: null,
104
+ verifiedAt: at,
105
+ host,
106
+ }
107
+ const writeErr = writeStateFile(statePath, envelope)
108
+ if (writeErr) {
109
+ return exitWith(
110
+ {
111
+ status: 'error',
112
+ command: 'verify-sandbox',
113
+ code: 'E_STATE_WRITE_FAILED',
114
+ verdict: 'needs_human',
115
+ reason: 'state_write_failed',
116
+ cause: writeErr.message,
117
+ project_dir: target,
118
+ state_path: statePath,
119
+ },
120
+ 1,
121
+ )
122
+ }
123
+ return exitWith(
124
+ {
125
+ status: 'error',
126
+ command: 'verify-sandbox',
127
+ code: 'E_NO_RECIPE',
128
+ project_dir: target,
129
+ state_path: statePath,
130
+ ...envelope,
131
+ },
132
+ 1,
133
+ )
134
+ }
135
+
136
+ let result
137
+ try {
138
+ result = await runSelfTest({ projectDir: target, recipeId, recipes })
139
+ } catch (e) {
140
+ const envelope = {
141
+ verified: false,
142
+ verdict: 'needs_human',
143
+ reason: 'self_test_failed_app_init_timeout',
144
+ recipeId,
145
+ recipeVersion,
146
+ verifiedAt: at,
147
+ host,
148
+ error: e?.message ?? String(e),
149
+ }
150
+ const writeErr = writeStateFile(statePath, envelope)
151
+ if (writeErr) {
152
+ return exitWith(
153
+ {
154
+ status: 'error',
155
+ command: 'verify-sandbox',
156
+ code: 'E_STATE_WRITE_FAILED',
157
+ verdict: 'needs_human',
158
+ reason: 'state_write_failed',
159
+ cause: writeErr.message,
160
+ project_dir: target,
161
+ state_path: statePath,
162
+ },
163
+ 1,
164
+ )
165
+ }
166
+ return exitWith(
167
+ {
168
+ status: 'error',
169
+ command: 'verify-sandbox',
170
+ project_dir: target,
171
+ state_path: statePath,
172
+ ...envelope,
173
+ },
174
+ 1,
175
+ )
176
+ }
177
+
178
+ const envelope = {
179
+ verified: result.verified,
180
+ verdict: result.verdict,
181
+ reason: result.reason,
182
+ recipeId,
183
+ recipeVersion,
184
+ verifiedAt: at,
185
+ host,
186
+ durationMs: result.durationMs,
187
+ }
188
+ const writeErr = writeStateFile(statePath, envelope)
189
+ if (writeErr) {
190
+ return exitWith(
191
+ {
192
+ status: 'error',
193
+ command: 'verify-sandbox',
194
+ code: 'E_STATE_WRITE_FAILED',
195
+ verdict: 'needs_human',
196
+ reason: 'state_write_failed',
197
+ cause: writeErr.message,
198
+ project_dir: target,
199
+ state_path: statePath,
200
+ },
201
+ 1,
202
+ )
203
+ }
204
+
205
+ const exitCode = result.verified ? 0 : 1
206
+ return exitWith(
207
+ {
208
+ status: result.verified ? 'ok' : 'error',
209
+ command: 'verify-sandbox',
210
+ project_dir: target,
211
+ state_path: statePath,
212
+ ...envelope,
213
+ },
214
+ exitCode,
215
+ )
216
+ }
217
+
218
+ /**
219
+ * Write the verdict envelope to the state file under a file lock.
220
+ * Returns null on success, or an Error if the lock or write fails.
221
+ */
222
+ function writeStateFile(statePath, envelope) {
223
+ try {
224
+ withFileLock(statePath, () => {
225
+ atomicWriteJson(statePath, envelope)
226
+ })
227
+ return null
228
+ } catch (e) {
229
+ return e instanceof Error ? e : new Error(String(e))
230
+ }
231
+ }
@@ -254,6 +254,7 @@ export function buildPolicyIgnoreMatcher(policy) {
254
254
  rest.startsWith('events/') ||
255
255
  rest.startsWith('locks/') ||
256
256
  rest.startsWith('team-status/') ||
257
+ rest.startsWith('last-seen/') ||
257
258
  rest.startsWith('paused/') ||
258
259
  rest.startsWith('rationalizations/') ||
259
260
  rest === 'digests/.last-run.json' ||
@@ -189,6 +189,7 @@ export function buildAperantGitignoreTemplate(policy = DEFAULT_SHARE_POLICY) {
189
189
  // maintainer cannot accidentally un-ignore it with a broad `!config*.json`
190
190
  // allow rule.
191
191
  lines.push('config.local.json')
192
+ lines.push('last-seen/')
192
193
  lines.push('')
193
194
  lines.push('# ── Derived markdown indexes (regenerated from JSON) ──────────────')
194
195
  lines.push('tasks/INDEX.md')
@@ -7,13 +7,17 @@
7
7
  * - flat findings: findings: [{severity}]
8
8
  * - legacy `issues: [{severity}]`
9
9
  *
10
- * Verdict closed set: {approved, approved-with-notes, rejected, needs-changes}.
11
- * Anything else -> 'unknown'. Accepts legacy `status` as a fallback field name.
10
+ * v0.9.0 (verify-proof-v2-outcome-walker_27-05-26) verdict closed set is
11
+ * `{approved, rejected, needs_human, needs-changes}`. `approved_with_notes`
12
+ * is DELETED (no shim) — reading a legacy `approved_with_notes` /
13
+ * `approved-with-notes` value throws a clear migration error pointing at
14
+ * the framework CHANGELOG entry. Anything else maps to `'unknown'`.
15
+ * Accepts legacy `status` as a fallback field name.
12
16
  *
13
17
  * Output: {verdict, findings: [{severity, title, status, file}]}
14
18
  */
15
19
 
16
- const KNOWN_VERDICTS = new Set(['approved', 'approved-with-notes', 'rejected', 'needs-changes'])
20
+ const KNOWN_VERDICTS = new Set(['approved', 'rejected', 'needs-human', 'needs-changes'])
17
21
 
18
22
  const SEVERITY_MAP = {
19
23
  critical: 'critical',
@@ -56,7 +60,19 @@ function normalizeVerdict(v) {
56
60
  const k = v.toLowerCase().trim()
57
61
  // normalize common spellings
58
62
  const canonical = k.replace(/_/g, '-')
59
- return KNOWN_VERDICTS.has(canonical) ? canonical : 'unknown'
63
+ // v0.9.0 migration: approved_with_notes / approved-with-notes is
64
+ // DELETED with no shim. Crash loudly so the consumer task is
65
+ // re-run, not silently re-mapped to `approved`.
66
+ if (canonical === 'approved-with-notes') {
67
+ throw new Error(
68
+ "qa_signoff.json carries the legacy verdict 'approved_with_notes' (or 'approved-with-notes'). " +
69
+ "This verdict is removed in framework v0.9.0+ (see packages/framework/CHANGELOG.md :: 'verify-proof v2 — outcome walker + two-gate verdict'). " +
70
+ 'Re-run verify-proof on this task to mint a fresh qa_signoff.json with the new {approved, rejected, needs_human} enum.',
71
+ )
72
+ }
73
+ // Accept both snake_case and kebab-case for needs_human.
74
+ const needsHuman = canonical === 'needs-human' ? 'needs-human' : canonical
75
+ return KNOWN_VERDICTS.has(needsHuman) ? needsHuman : 'unknown'
60
76
  }
61
77
 
62
78
  function collectRaw(qa) {
@@ -3,13 +3,15 @@
3
3
  * consistency/rules/r5-verdict-consistency.mjs — R5 (C15).
4
4
  *
5
5
  * Fails when:
6
- * - qa.verdict === 'approved' or 'approved-with-notes' AND review has >=1
7
- * unresolved critical or major finding
8
- * - qa.verdict === 'rejected' AND qa has no findings at all AND review has
9
- * zero unresolved findings (rejected-without-evidence)
6
+ * - qa.verdict === 'approved' AND review has >=1 unresolved critical or
7
+ * major finding.
8
+ * - qa.verdict === 'rejected' AND qa has no findings at all AND review
9
+ * has zero unresolved findings (rejected-without-evidence).
10
10
  *
11
- * `approved-with-notes` no longer escapes unresolved critical or major
12
- * findings (G42 Phase 1). It still passes for lower-severity issues.
11
+ * v0.9.0 (verify-proof-v2-outcome-walker_27-05-26) the legacy
12
+ * `approved-with-notes` branch is REMOVED. The verdict enum is the closed
13
+ * set `{approved, rejected, needs_human}`. Reading a legacy verdict
14
+ * crashes in parseQaSignoff before this rule executes.
13
15
  *
14
16
  * Severity: error. Stages: verify, ship.
15
17
  */
@@ -30,10 +32,7 @@ const rule = {
30
32
  const unresolvedBlocking = reviewFindings.filter(
31
33
  (f) => (f.severity === 'critical' || f.severity === 'major') && f.status !== 'fixed',
32
34
  )
33
- if (
34
- (verdict === 'approved' || verdict === 'approved-with-notes') &&
35
- unresolvedBlocking.length > 0
36
- ) {
35
+ if (verdict === 'approved' && unresolvedBlocking.length > 0) {
37
36
  return {
38
37
  status: 'fail',
39
38
  drift: [
@@ -43,11 +42,10 @@ const rule = {
43
42
  id: f.id,
44
43
  title: f.title,
45
44
  })),
46
- reason:
47
- 'approved/approved-with-notes verdict with unresolved critical or major finding(s) (G42 Phase 1)',
45
+ reason: 'approved verdict with unresolved critical or major finding(s) (G42 Phase 1)',
48
46
  },
49
47
  ],
50
- reason: `approved/approved-with-notes verdict contradicts ${unresolvedBlocking.length} unresolved critical/major finding(s)`,
48
+ reason: `approved verdict contradicts ${unresolvedBlocking.length} unresolved critical/major finding(s)`,
51
49
  }
52
50
  }
53
51
  if (verdict === 'rejected') {