@aperant/framework 0.7.0 → 0.7.4

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 (151) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/agents/apt-planner.md +12 -0
  3. package/agents/apt-pr-review-fixer.md +13 -9
  4. package/bin/apt-tools.mjs +7 -0
  5. package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -1
  6. package/dist/cli/ci-watch/stop-matrix.mjs +16 -0
  7. package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -1
  8. package/dist/cli/commands/ci-watch.d.mts +11 -0
  9. package/dist/cli/commands/ci-watch.d.mts.map +1 -1
  10. package/dist/cli/commands/ci-watch.mjs +108 -2
  11. package/dist/cli/commands/ci-watch.mjs.map +1 -1
  12. package/dist/cli/commands/features-audit.d.mts +24 -0
  13. package/dist/cli/commands/features-audit.d.mts.map +1 -1
  14. package/dist/cli/commands/features-audit.mjs +159 -5
  15. package/dist/cli/commands/features-audit.mjs.map +1 -1
  16. package/dist/cli/commands/health-check.d.mts +16 -0
  17. package/dist/cli/commands/health-check.d.mts.map +1 -1
  18. package/dist/cli/commands/health-check.mjs +118 -2
  19. package/dist/cli/commands/health-check.mjs.map +1 -1
  20. package/dist/cli/commands/init.d.mts +9 -0
  21. package/dist/cli/commands/init.d.mts.map +1 -1
  22. package/dist/cli/commands/init.mjs +49 -4
  23. package/dist/cli/commands/init.mjs.map +1 -1
  24. package/dist/cli/commands/modes.d.mts.map +1 -1
  25. package/dist/cli/commands/modes.mjs +11 -0
  26. package/dist/cli/commands/modes.mjs.map +1 -1
  27. package/dist/cli/commands/pr-review-audit-fixer.d.mts +13 -0
  28. package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
  29. package/dist/cli/commands/pr-review-audit-fixer.mjs +18 -5
  30. package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
  31. package/dist/cli/commands/task.d.mts.map +1 -1
  32. package/dist/cli/commands/task.mjs +84 -5
  33. package/dist/cli/commands/task.mjs.map +1 -1
  34. package/dist/cli/commands/validate-evidence.d.mts +24 -2
  35. package/dist/cli/commands/validate-evidence.d.mts.map +1 -1
  36. package/dist/cli/commands/validate-evidence.mjs +154 -17
  37. package/dist/cli/commands/validate-evidence.mjs.map +1 -1
  38. package/dist/cli/commands/vitest-doctor.d.mts +2 -0
  39. package/dist/cli/commands/vitest-doctor.d.mts.map +1 -0
  40. package/dist/cli/commands/vitest-doctor.mjs +168 -0
  41. package/dist/cli/commands/vitest-doctor.mjs.map +1 -0
  42. package/dist/cli/config/gitignore-drift.d.mts +23 -1
  43. package/dist/cli/config/gitignore-drift.d.mts.map +1 -1
  44. package/dist/cli/config/gitignore-drift.mjs +81 -3
  45. package/dist/cli/config/gitignore-drift.mjs.map +1 -1
  46. package/dist/cli/config/load.d.mts +56 -2
  47. package/dist/cli/config/load.d.mts.map +1 -1
  48. package/dist/cli/config/load.mjs +192 -2
  49. package/dist/cli/config/load.mjs.map +1 -1
  50. package/dist/cli/consistency/parse-review.mjs +7 -0
  51. package/dist/cli/consistency/parse-review.mjs.map +1 -1
  52. package/dist/cli/dispatch.d.mts.map +1 -1
  53. package/dist/cli/dispatch.mjs +24 -2
  54. package/dist/cli/dispatch.mjs.map +1 -1
  55. package/dist/cli/gate/gates/gitignore-in-sync.d.mts +1 -1
  56. package/dist/cli/gate/gates/gitignore-in-sync.d.mts.map +1 -1
  57. package/dist/cli/gate/gates/gitignore-in-sync.mjs +5 -2
  58. package/dist/cli/gate/gates/gitignore-in-sync.mjs.map +1 -1
  59. package/dist/cli/gate/gates/review-clean.d.mts +5 -1
  60. package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
  61. package/dist/cli/gate/gates/review-clean.mjs +23 -18
  62. package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
  63. package/dist/cli/gate/gates/verify-approved.d.mts +49 -1
  64. package/dist/cli/gate/gates/verify-approved.d.mts.map +1 -1
  65. package/dist/cli/gate/gates/verify-approved.mjs +93 -14
  66. package/dist/cli/gate/gates/verify-approved.mjs.map +1 -1
  67. package/dist/cli/help.d.mts.map +1 -1
  68. package/dist/cli/help.mjs +7 -2
  69. package/dist/cli/help.mjs.map +1 -1
  70. package/dist/cli/install/runtime-detect.d.mts +13 -0
  71. package/dist/cli/install/runtime-detect.d.mts.map +1 -1
  72. package/dist/cli/install/runtime-detect.mjs +9 -0
  73. package/dist/cli/install/runtime-detect.mjs.map +1 -1
  74. package/dist/cli/task/index-md.d.mts.map +1 -1
  75. package/dist/cli/task/index-md.mjs +14 -2
  76. package/dist/cli/task/index-md.mjs.map +1 -1
  77. package/dist/plugin/.claude-plugin/plugin.json +11 -2
  78. package/dist/plugin/agents/apt-improver.md +99 -0
  79. package/dist/plugin/agents/apt-planner.md +127 -10
  80. package/dist/plugin/agents/apt-pr-review-fixer.md +13 -9
  81. package/dist/plugin/skills/apt/SKILL.md +1 -0
  82. package/dist/plugin/skills/apt-close-task/SKILL.md +63 -1
  83. package/dist/plugin/skills/apt-debug/SKILL.md +39 -6
  84. package/dist/plugin/skills/apt-debug/appendices/diagnose-discipline.md +119 -0
  85. package/dist/plugin/skills/apt-diagram/SKILL.md +378 -0
  86. package/dist/plugin/skills/apt-diagram/appendices/design-discipline.md +97 -0
  87. package/dist/plugin/skills/apt-discuss/SKILL.md +72 -5
  88. package/dist/plugin/skills/apt-discuss/appendices/grill-discipline.md +104 -0
  89. package/dist/plugin/skills/apt-discuss/appendices/zoom-out-helper.md +79 -0
  90. package/dist/plugin/skills/apt-execute/SKILL.md +57 -5
  91. package/dist/plugin/skills/apt-execute/appendices/tdd-mode.md +107 -0
  92. package/dist/plugin/skills/apt-improve/DEEPENING.md +84 -0
  93. package/dist/plugin/skills/apt-improve/INTERFACE-DESIGN.md +97 -0
  94. package/dist/plugin/skills/apt-improve/LANGUAGE.md +104 -0
  95. package/dist/plugin/skills/apt-improve/SKILL.md +141 -0
  96. package/dist/plugin/skills/apt-plan/SKILL.md +171 -4
  97. package/dist/plugin/skills/apt-plan/adapters/conductor.md +98 -0
  98. package/dist/plugin/skills/apt-pr-review/SKILL.md +57 -18
  99. package/dist/plugin/skills/apt-prototype/LOGIC.md +109 -0
  100. package/dist/plugin/skills/apt-prototype/SKILL.md +143 -0
  101. package/dist/plugin/skills/apt-prototype/UI.md +90 -0
  102. package/dist/plugin/skills/apt-quick/SKILL.md +49 -8
  103. package/dist/plugin/skills/apt-release-notes/SKILL.md +193 -0
  104. package/dist/plugin/skills/apt-release-notes/appendices/persona-voice.md +59 -0
  105. package/dist/plugin/skills/apt-review/SKILL.md +2 -0
  106. package/dist/plugin/skills/apt-run/SKILL.md +32 -4
  107. package/dist/plugin/skills/apt-setup/SKILL.md +308 -6
  108. package/dist/plugin/skills/apt-ship/SKILL.md +122 -1
  109. package/dist/plugin/skills/apt-spar/SKILL.md +315 -0
  110. package/dist/plugin/skills/apt-triage/AGENT-BRIEF.md +84 -0
  111. package/dist/plugin/skills/apt-triage/OUT-OF-SCOPE.md +75 -0
  112. package/dist/plugin/skills/apt-triage/SKILL.md +169 -0
  113. package/dist/plugin/skills/apt-update/SKILL.md +77 -10
  114. package/dist/plugin/skills/apt-verify/SKILL.md +3 -0
  115. package/dist/plugin/skills/apt-verify-proof/SKILL.md +10 -5
  116. package/dist/plugin/skills/apt-watch-ci/SKILL.md +166 -0
  117. package/dist/plugin/skills/apt-zoom-out/SKILL.md +130 -0
  118. package/package.json +1 -1
  119. package/prompts/conductor-framework-context.md +63 -0
  120. package/prompts/conductor-system.md +11 -0
  121. package/skills/apt-close-task/SKILL.md +1 -0
  122. package/skills/apt-discuss/SKILL.md +47 -5
  123. package/skills/apt-execute/SKILL.md +9 -0
  124. package/skills/apt-plan/SKILL.md +12 -0
  125. package/skills/apt-pr-review/SKILL.md +19 -3
  126. package/skills/apt-quick/SKILL.md +19 -8
  127. package/skills/apt-researcher.md +1 -0
  128. package/skills/apt-setup/SKILL.md +33 -2
  129. package/skills/apt-verify-proof/SKILL.md +7 -5
  130. package/src/cli/ci-watch/stop-matrix.mjs +17 -0
  131. package/src/cli/commands/ci-watch.mjs +113 -2
  132. package/src/cli/commands/features-audit.mjs +164 -5
  133. package/src/cli/commands/health-check.mjs +115 -2
  134. package/src/cli/commands/init.mjs +52 -4
  135. package/src/cli/commands/modes.mjs +11 -0
  136. package/src/cli/commands/pr-review-audit-fixer.mjs +18 -5
  137. package/src/cli/commands/task.mjs +83 -5
  138. package/src/cli/commands/validate-evidence.mjs +158 -17
  139. package/src/cli/commands/vitest-doctor.mjs +173 -0
  140. package/src/cli/config/gitignore-drift.mjs +74 -3
  141. package/src/cli/config/load.mjs +188 -2
  142. package/src/cli/consistency/parse-review.mjs +6 -0
  143. package/src/cli/dispatch.mjs +23 -2
  144. package/src/cli/gate/gates/gitignore-in-sync.mjs +5 -2
  145. package/src/cli/gate/gates/review-clean.mjs +24 -19
  146. package/src/cli/gate/gates/verify-approved.mjs +97 -14
  147. package/src/cli/help.mjs +7 -2
  148. package/src/cli/install/runtime-detect.mjs +9 -0
  149. package/src/cli/task/index-md.mjs +15 -2
  150. package/templates/config.json +2 -7
  151. package/workflows/verify-proof.md +8 -3
@@ -2,7 +2,7 @@
2
2
  * config/load.mjs — project config loader + task-isolation normalizer (C29).
3
3
  */
4
4
 
5
- import { existsSync, readFileSync } from 'node:fs'
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
6
6
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
7
7
  import { fileURLToPath } from 'node:url'
8
8
  import { cliToInstallRoot } from '../install/cli-to-install-root.mjs'
@@ -31,7 +31,7 @@ export function loadProjectConfig(targetDir) {
31
31
  * fallback would mask the user's broken override. See FRAMEWORK-AUDIT-001.
32
32
  *
33
33
  * @param {string} targetDir
34
- * @returns {object | null}
34
+ * @returns {Record<string, any> | null}
35
35
  */
36
36
  export function loadMergedProjectConfig(targetDir) {
37
37
  const shared = loadProjectConfig(targetDir)
@@ -57,6 +57,192 @@ function deepMerge(a, b) {
57
57
  return out
58
58
  }
59
59
 
60
+ /**
61
+ * AUDIT-001 — read+write helpers for `.aperant/config.local.json`.
62
+ * Used by the 5 deferred fields migrated out of `config.json`:
63
+ * - verification.open_report
64
+ * - multi_model.cross_verification.tools
65
+ * - preferences.date_format
66
+ * - preferences.time_format
67
+ * - preferences.quick_task_post_verify
68
+ *
69
+ * Each field has its own read/write site. The shared helpers below give
70
+ * those sites a single primitive instead of hand-rolled JSON I/O.
71
+ */
72
+ function setDottedPath(obj, dotPath, value) {
73
+ const parts = dotPath.split('.')
74
+ let cursor = obj
75
+ for (let i = 0; i < parts.length - 1; i++) {
76
+ const key = parts[i]
77
+ if (!cursor[key] || typeof cursor[key] !== 'object' || Array.isArray(cursor[key])) {
78
+ cursor[key] = {}
79
+ }
80
+ cursor = cursor[key]
81
+ }
82
+ cursor[parts[parts.length - 1]] = value
83
+ }
84
+
85
+ function getDottedPath(obj, dotPath) {
86
+ const parts = dotPath.split('.')
87
+ let cursor = obj
88
+ for (const part of parts) {
89
+ if (cursor === null || cursor === undefined || typeof cursor !== 'object') return undefined
90
+ cursor = cursor[part]
91
+ }
92
+ return cursor
93
+ }
94
+
95
+ function deleteDottedPath(obj, dotPath) {
96
+ const parts = dotPath.split('.')
97
+ let cursor = obj
98
+ for (let i = 0; i < parts.length - 1; i++) {
99
+ const key = parts[i]
100
+ if (!cursor[key] || typeof cursor[key] !== 'object') return false
101
+ cursor = cursor[key]
102
+ }
103
+ const leaf = parts[parts.length - 1]
104
+ if (leaf in cursor) {
105
+ delete cursor[leaf]
106
+ return true
107
+ }
108
+ return false
109
+ }
110
+
111
+ /**
112
+ * Read a single dotted-path field from the merged config (local overrides
113
+ * shared). Returns `undefined` when neither file carries the path.
114
+ *
115
+ * @internal
116
+ */
117
+ export function readLocalOrSharedField(targetDir, dotPath) {
118
+ const merged = loadMergedProjectConfig(targetDir)
119
+ if (!merged) return undefined
120
+ return getDottedPath(merged, dotPath)
121
+ }
122
+
123
+ /**
124
+ * Write a single dotted-path field into `.aperant/config.local.json`,
125
+ * creating the file (and intermediate path) when it doesn't exist.
126
+ *
127
+ * The shared `config.json` is NOT touched by this helper — that's the
128
+ * load-bearing AUDIT-001 contract. Callers that need to remove a stale
129
+ * entry from `config.json` (e.g. the init.mjs migrator) must do so
130
+ * explicitly.
131
+ *
132
+ * @internal
133
+ */
134
+ export function writeLocalConfigField(targetDir, dotPath, value) {
135
+ const aperantDir = join(targetDir, '.aperant')
136
+ mkdirSync(aperantDir, { recursive: true })
137
+ const localPath = join(aperantDir, 'config.local.json')
138
+ let local = {}
139
+ if (existsSync(localPath)) {
140
+ try {
141
+ local = JSON.parse(readFileSync(localPath, 'utf-8'))
142
+ if (local === null || typeof local !== 'object' || Array.isArray(local)) local = {}
143
+ } catch (e) {
144
+ throw new Error(`config.local.json parse error: ${e.message}`)
145
+ }
146
+ }
147
+ setDottedPath(local, dotPath, value)
148
+ writeFileSync(localPath, `${JSON.stringify(local, null, 2)}\n`)
149
+ }
150
+
151
+ /**
152
+ * AUDIT-001 — first-run migrator. For each dotted-path entry present in
153
+ * `.aperant/config.json`, copy the value into `.aperant/config.local.json`
154
+ * (idempotent — won't overwrite an existing local value) and delete the
155
+ * entry from `config.json`. Each field's migration is independent —
156
+ * one failure does NOT block the others (a per-field try/catch wraps
157
+ * each move).
158
+ *
159
+ * Returns an array `[{ field, migrated_value, status }]` for caller
160
+ * telemetry; `status` is `'migrated' | 'already-local' | 'absent' | 'error'`.
161
+ *
162
+ * @internal
163
+ */
164
+ export function migrateDeferredFieldsToLocalConfig(targetDir, dotPaths) {
165
+ const results = []
166
+ const sharedPath = join(targetDir, '.aperant', 'config.json')
167
+ if (!existsSync(sharedPath)) {
168
+ for (const dotPath of dotPaths) results.push({ field: dotPath, status: 'absent' })
169
+ return results
170
+ }
171
+ let shared
172
+ try {
173
+ shared = JSON.parse(readFileSync(sharedPath, 'utf-8'))
174
+ } catch {
175
+ for (const dotPath of dotPaths) results.push({ field: dotPath, status: 'error' })
176
+ return results
177
+ }
178
+ // Pre-load local config ONCE so the already-local check reads from
179
+ // local-only state (NOT from the merged loader — that would conflate
180
+ // the shared entry we're about to delete with a real local override).
181
+ const localPath = join(targetDir, '.aperant', 'config.local.json')
182
+ let localAtStart = {}
183
+ if (existsSync(localPath)) {
184
+ try {
185
+ localAtStart = JSON.parse(readFileSync(localPath, 'utf-8')) ?? {}
186
+ } catch {
187
+ localAtStart = {}
188
+ }
189
+ }
190
+ let sharedMutated = false
191
+ for (const dotPath of dotPaths) {
192
+ try {
193
+ const sharedValue = getDottedPath(shared, dotPath)
194
+ if (sharedValue === undefined) {
195
+ results.push({ field: dotPath, status: 'absent' })
196
+ continue
197
+ }
198
+ const existingLocal = getDottedPath(localAtStart, dotPath)
199
+ // Already-local: the local override already exists. Strip the
200
+ // stale shared entry but keep the local value as-is (don't
201
+ // overwrite a user-tuned local with the stale shared default).
202
+ if (existingLocal !== undefined) {
203
+ deleteDottedPath(shared, dotPath)
204
+ sharedMutated = true
205
+ results.push({ field: dotPath, status: 'already-local', migrated_value: sharedValue })
206
+ continue
207
+ }
208
+ writeLocalConfigField(targetDir, dotPath, sharedValue)
209
+ deleteDottedPath(shared, dotPath)
210
+ sharedMutated = true
211
+ results.push({ field: dotPath, status: 'migrated', migrated_value: sharedValue })
212
+ } catch (e) {
213
+ results.push({ field: dotPath, status: 'error', error: e?.message ?? String(e) })
214
+ }
215
+ }
216
+ if (sharedMutated) {
217
+ try {
218
+ writeFileSync(sharedPath, `${JSON.stringify(shared, null, 2)}\n`)
219
+ } catch (e) {
220
+ // Disk write failed after per-field local writes succeeded.
221
+ // Log a structured warning so callers can surface the partial state
222
+ // to the user (config.local.json updated, config.json NOT cleaned up).
223
+ results.push({
224
+ field: '__write_shared__',
225
+ status: 'error',
226
+ error: `migration_partial_rollback: config.local.json was updated but config.json could not be written (${e?.message ?? String(e)}). Re-run init to retry.`,
227
+ })
228
+ }
229
+ }
230
+ return results
231
+ }
232
+
233
+ /**
234
+ * AUDIT-001 — the 5 deferred fields that move to config.local.json.
235
+ * Single source of truth used by both the init migrator and the
236
+ * per-field regression tests.
237
+ */
238
+ export const AUDIT_001_LOCAL_FIELDS = Object.freeze([
239
+ 'verification.open_report',
240
+ 'multi_model.cross_verification.tools',
241
+ 'preferences.date_format',
242
+ 'preferences.time_format',
243
+ 'preferences.quick_task_post_verify',
244
+ ])
245
+
60
246
  /**
61
247
  * Return a normalized task_isolation config object with safe fallbacks.
62
248
  * Always returns a concrete object even when the project has no config at all,
@@ -95,6 +95,12 @@ function normalizeStatus(f) {
95
95
  if (typeof f.status === 'string') {
96
96
  const s = f.status.toLowerCase().trim()
97
97
  if (STATUS_MAP[s]) return STATUS_MAP[s]
98
+ // BUG-009: reviewer-skill writes `deferred-per-spec`, `deferred-followup`,
99
+ // etc. for findings that are intentionally not being fixed in the
100
+ // current scope. Collapse the variant prefix back to canonical
101
+ // `deferred` so downstream gates (review-clean, ship) treat them
102
+ // the same as a bare `status: "deferred"`.
103
+ if (s.startsWith('deferred-')) return 'deferred'
98
104
  }
99
105
  return 'open'
100
106
  }
@@ -41,7 +41,7 @@ import { cmdAuthorSkill } from './commands/author-skill.mjs'
41
41
  import { cmdBootstrap } from './commands/bootstrap.mjs'
42
42
  import { cmdCheck } from './commands/check.mjs'
43
43
  import { cmdCheckVersion } from './commands/check-version.mjs'
44
- import { cmdCiWatch } from './commands/ci-watch.mjs'
44
+ import { cmdCiWatch, cmdCiWatchSweep } from './commands/ci-watch.mjs'
45
45
  import { cmdCommit } from './commands/commit.mjs'
46
46
  import { cmdConsistency } from './commands/consistency.mjs'
47
47
  import { cmdConstitution } from './commands/constitution.mjs'
@@ -93,6 +93,7 @@ import { cmdValidateEvidence } from './commands/validate-evidence.mjs'
93
93
  import { cmdVariantGallery } from './commands/variant-gallery.mjs'
94
94
  import { cmdVariantPick } from './commands/variant-pick.mjs'
95
95
  import { cmdVariantPool } from './commands/variant-pool.mjs'
96
+ import { cmdVitestDoctor } from './commands/vitest-doctor.mjs'
96
97
  import { cmdWave } from './commands/wave.mjs'
97
98
  import { cmdWfrun } from './commands/wfrun.mjs'
98
99
  import { cmdWorkflow } from './commands/workflow.mjs'
@@ -241,6 +242,21 @@ export const DISPATCH = {
241
242
  'precompact-hook': (args) => wrap(cmdPrecompactHook)(args[1]),
242
243
  'migrate-pr-reviews': (args) => wrap(cmdMigratePrReviews)(args[1]),
243
244
  'validate-evidence': (args) => wrap(cmdValidateEvidence)(args[1], args.slice(2)),
245
+ vitest: (args) => {
246
+ // `vitest doctor <project-dir>` — currently the only `vitest`
247
+ // subcommand. Reserved namespace for future vitest diagnostics.
248
+ const subcommand = args[1]
249
+ if (subcommand === 'doctor') {
250
+ return wrap(cmdVitestDoctor)(args[2], args.slice(3))
251
+ }
252
+ return wrap(() => ({
253
+ envelope: {
254
+ status: 'error',
255
+ error: `Usage: apt-tools vitest doctor <project-dir>. Unknown subcommand: ${subcommand ?? '(none)'}`,
256
+ },
257
+ exitCode: 1,
258
+ }))()
259
+ },
244
260
 
245
261
  // ---- (positional dir, args[2] subcommand, slice(3)) shape ----
246
262
  tokens: (args) => wrap(cmdTokens)(args[1], args[2], args.slice(3)),
@@ -261,7 +277,12 @@ export const DISPATCH = {
261
277
  seed: (args) => wrap(cmdSeed)(args[1], args[2], args.slice(3)),
262
278
  workflow: (args) => wrap(cmdWorkflow)(args[1], args[2], args.slice(3)),
263
279
  'pr-review': (args) => wrap(cmdPrReview)(args[1], args[2], args.slice(3)),
264
- 'ci-watch': (args) => wrap(cmdCiWatch)(args[1], args[2], args.slice(3)),
280
+ 'ci-watch': (args) => {
281
+ // `sweep` is async; route it to the async sibling so the sync dispatcher
282
+ // path doesn't return a CommandResult | Promise<CommandResult> union.
283
+ if (args[1] === 'sweep') return wrap(cmdCiWatchSweep)(args[2], args.slice(3))
284
+ return wrap(cmdCiWatch)(args[1], args[2], args.slice(3))
285
+ },
265
286
  'plan-approval': (args) => wrap(cmdPlanApproval)(args[1], args[2], args.slice(3)),
266
287
 
267
288
  // ---- Pocock adoption (ST-5, ST-6, ST-10, ST-13) artifact writers + discoverability ----
@@ -22,10 +22,13 @@ const gate = {
22
22
  ]
23
23
  const result = checkGitignoreDrift(ctx.sharedRoot)
24
24
  if (result.drifted) {
25
+ // BUG-028: surface the unified-diff summary in the reason so
26
+ // users see exactly which template lines drifted without
27
+ // re-running `apt-tools config upgrade-gitignore --dry-run`.
25
28
  return {
26
29
  status: 'fail',
27
- reason: '.aperant/.gitignore has drifted from policy-expected template',
28
- evidence: { reason: result.reason ?? null },
30
+ reason: result.reason ?? '.aperant/.gitignore has drifted from policy-expected template',
31
+ evidence: { diff: result.diff ?? null },
29
32
  inputs,
30
33
  }
31
34
  }
@@ -3,24 +3,27 @@
3
3
  * G5 review-clean — blocking gate for `apt:ship`.
4
4
  *
5
5
  * Skipped when {task_dir}/review.json is absent (review is optional for some
6
- * tracks). When it is present, fails if any finding has
7
- * severity {"critical", "warning"} AND neither `resolved === true` nor
8
- * `fixed === true`. The reviewer skill writes resolution as `fixed: true`
9
- * paired with `fix_commit`; `resolved: true` is a legacy alias.
10
- *
11
- * The findings array may be under `findings` or `issues` depending on the
12
- * reviewer format — we accept either.
6
+ * tracks). When it is present, delegates to `parseReview` (DRY with the
7
+ * consistency rule) which normalizes severities + statuses across all the
8
+ * shapes the reviewer skill writes: `fixed: true`, `resolved: true`,
9
+ * `status: "fixed"` (string), and `status: "deferred-*"` variants
10
+ * (BUG-009). Fails if any post-normalization finding is severity
11
+ * critical|major AND status is neither `fixed` nor `deferred`.
13
12
  */
14
13
 
15
14
  import { existsSync, readFileSync } from 'node:fs'
16
15
  import { join } from 'node:path'
16
+ import { parseReview } from '../../consistency/parse-review.mjs'
17
17
  import { registerGate } from '../registry.mjs'
18
18
 
19
- const BLOCKING_SEVERITIES = new Set(['critical', 'warning'])
19
+ // Post-normalization severities. `parseReview` maps `warning → major`,
20
+ // so the gate keys off `major` here even though the input may carry
21
+ // `warning`.
22
+ const POST_NORM_BLOCKING = new Set(['critical', 'major'])
20
23
 
21
24
  const gate = {
22
25
  id: 'review-clean',
23
- description: 'review.json has no unresolved critical/warning findings (skipped if absent)',
26
+ description: 'review.json has no unresolved critical/major findings (skipped if absent)',
24
27
  severity: 'blocking',
25
28
  skills: ['ship'],
26
29
  async check(ctx) {
@@ -43,23 +46,25 @@ const gate = {
43
46
  inputs,
44
47
  }
45
48
  }
46
- const findings = Array.isArray(doc.findings)
47
- ? doc.findings
48
- : Array.isArray(doc.issues)
49
- ? doc.issues
50
- : []
51
- const unresolved = findings.filter(
52
- (f) => BLOCKING_SEVERITIES.has(f?.severity) && f?.resolved !== true && f?.fixed !== true,
49
+ // BUG-009: delegate to parseReview so this gate sees the SAME
50
+ // normalized findings the consistency rule does. The pre-fix
51
+ // inline filter only inspected boolean `fixed`/`resolved`, missing
52
+ // `status: "fixed"` strings + `summary.findings_remaining: 0`
53
+ // shapes the reviewer skill writes; this delegation closes that
54
+ // hole and picks up `deferred-*` variants for free.
55
+ const { findings: normalized } = parseReview(doc)
56
+ const unresolved = normalized.filter(
57
+ (f) => POST_NORM_BLOCKING.has(f.severity) && f.status !== 'fixed' && f.status !== 'deferred',
53
58
  )
54
59
  if (unresolved.length > 0) {
55
60
  return {
56
61
  status: 'fail',
57
- reason: `${unresolved.length} unresolved critical/warning finding(s) in review.json`,
62
+ reason: `${unresolved.length} unresolved critical/major finding(s) in review.json`,
58
63
  evidence: {
59
64
  unresolved: unresolved.map((f) => ({
60
65
  severity: f.severity,
61
66
  id: f.id ?? null,
62
- title: f.title ?? f.message ?? null,
67
+ title: f.title ?? null,
63
68
  })),
64
69
  },
65
70
  inputs,
@@ -67,7 +72,7 @@ const gate = {
67
72
  }
68
73
  return {
69
74
  status: 'pass',
70
- reason: `review.json has no unresolved critical/warning findings (${findings.length} total)`,
75
+ reason: `review.json has no unresolved critical/major findings (${normalized.length} total)`,
71
76
  inputs,
72
77
  }
73
78
  },
@@ -3,22 +3,54 @@
3
3
  * G4 verify-approved — blocking gate for `apt:ship`.
4
4
  *
5
5
  * Requires {task_dir}/qa_signoff.json to exist, parse, and carry
6
- * verdict === "approved". The ship skill's existing prose only asked the
7
- * agent to "confirm" approval; this gate enforces it deterministically.
6
+ * verdict === "approved" (BUG-013: normalized via parseQaSignoff case
7
+ * and underscore/hyphen variants collapse to canonical). BUG-012: also
8
+ * accepts `approved-with-notes` when config.qa.approved_with_notes_threshold
9
+ * is set AND every dimension score meets the threshold (best-effort when
10
+ * dimensions are missing or in an unknown shape).
8
11
  *
9
- * NOTE: the file may use either `verdict` (newer) or `status` (older) field
10
- * name for the QA result. We accept both for forwards/backwards compatibility,
11
- * but only the string "approved" passes — "approved-with-notes" fails
12
- * because ship-time deserves the strongest signal.
12
+ * Accepts both `verdict` (newer) and `status` (older) field names — that
13
+ * normalization is delegated to parseQaSignoff.
13
14
  */
14
15
 
15
16
  import { existsSync, readFileSync } from 'node:fs'
16
17
  import { join } from 'node:path'
18
+ import { parseQaSignoff } from '../../consistency/parse-qa.mjs'
17
19
  import { registerGate } from '../registry.mjs'
18
20
 
21
+ /**
22
+ * Extract dimension scores from the qa_signoff.json shape. We've seen two
23
+ * shapes in the wild:
24
+ * - Array: `dimensions: [{name: 'security', score: 0.9}, ...]`
25
+ * - Object: `dimensions: {security: 0.9, performance: 0.85}` OR
26
+ * `dimensions: {security: {score: 0.9}, ...}`
27
+ * Returns an array of numeric scores, or null when no recognizable shape
28
+ * is present (best-effort accept per brief).
29
+ */
30
+ function extractDimensionScores(doc) {
31
+ const dims = doc.dimensions ?? doc.qa?.dimensions ?? null
32
+ if (!dims) return null
33
+ if (Array.isArray(dims)) {
34
+ const scores = dims
35
+ .map((d) => (typeof d?.score === 'number' ? d.score : null))
36
+ .filter((s) => s !== null)
37
+ return scores.length > 0 ? scores : null
38
+ }
39
+ if (typeof dims === 'object') {
40
+ const scores = []
41
+ for (const v of Object.values(dims)) {
42
+ if (typeof v === 'number') scores.push(v)
43
+ else if (typeof v?.score === 'number') scores.push(v.score)
44
+ }
45
+ return scores.length > 0 ? scores : null
46
+ }
47
+ return null
48
+ }
49
+
19
50
  const gate = {
20
51
  id: 'verify-approved',
21
- description: 'qa_signoff.json exists with verdict === "approved"',
52
+ description:
53
+ 'qa_signoff.json exists with verdict === "approved" (or "approved-with-notes" when configured)',
22
54
  severity: 'blocking',
23
55
  skills: ['ship'],
24
56
  async check(ctx) {
@@ -41,18 +73,69 @@ const gate = {
41
73
  inputs,
42
74
  }
43
75
  }
44
- const verdict = doc.verdict ?? doc.status ?? null
45
- if (verdict !== 'approved') {
76
+
77
+ // BUG-013: route through parseQaSignoff so the verdict is
78
+ // case-insensitive AND tolerates `_↔-` (so 'APPROVED', 'Approved',
79
+ // ' approved ', 'APPROVED_WITH_NOTES' all behave canonically). The
80
+ // pre-fix gate compared the raw string against `'approved'` which
81
+ // false-rejected every legitimate variant.
82
+ const { verdict } = parseQaSignoff(doc)
83
+
84
+ if (verdict === 'approved') {
46
85
  return {
47
- status: 'fail',
48
- reason: `qa_signoff.json verdict is "${verdict}" (expected "approved")`,
49
- evidence: { verdict },
86
+ status: 'pass',
87
+ reason: 'qa_signoff.json verdict normalized to "approved"',
50
88
  inputs,
51
89
  }
52
90
  }
91
+
92
+ // BUG-012: accept `approved-with-notes` when the project has opted
93
+ // in via config.qa.approved_with_notes_threshold. The threshold is
94
+ // the minimum per-dimension score required. Best-effort: when
95
+ // dimensions are missing / in an unknown shape we still accept and
96
+ // log, rather than hard-failing, so projects that haven't started
97
+ // scoring dimensions yet aren't blocked.
98
+ if (verdict === 'approved-with-notes') {
99
+ const threshold = ctx.config?.qa?.approved_with_notes_threshold
100
+ if (typeof threshold !== 'number') {
101
+ return {
102
+ status: 'fail',
103
+ reason:
104
+ 'qa_signoff.json verdict is "approved-with-notes" but config.qa.approved_with_notes_threshold is not set',
105
+ evidence: { verdict, threshold: threshold ?? null },
106
+ inputs,
107
+ }
108
+ }
109
+ const scores = extractDimensionScores(doc)
110
+ if (scores === null) {
111
+ return {
112
+ status: 'pass',
113
+ reason: `qa_signoff.json verdict is "approved-with-notes" and threshold ${threshold} is enabled; dimensions missing or unknown-shape — accepting best-effort`,
114
+ evidence: { verdict, threshold, dimensions_present: false },
115
+ inputs,
116
+ }
117
+ }
118
+ const failing = scores.filter((s) => s < threshold)
119
+ if (failing.length > 0) {
120
+ return {
121
+ status: 'fail',
122
+ reason: `qa_signoff.json verdict is "approved-with-notes" but ${failing.length}/${scores.length} dimension score(s) below threshold ${threshold}`,
123
+ evidence: { verdict, threshold, scores, failing },
124
+ inputs,
125
+ }
126
+ }
127
+ return {
128
+ status: 'pass',
129
+ reason: `qa_signoff.json verdict is "approved-with-notes" and all ${scores.length} dimension score(s) meet threshold ${threshold}`,
130
+ evidence: { verdict, threshold, scores },
131
+ inputs,
132
+ }
133
+ }
134
+
53
135
  return {
54
- status: 'pass',
55
- reason: 'qa_signoff.json.verdict === "approved"',
136
+ status: 'fail',
137
+ reason: `qa_signoff.json verdict is "${verdict}" (expected "approved" or "approved-with-notes" with threshold)`,
138
+ evidence: { verdict },
56
139
  inputs,
57
140
  }
58
141
  },
package/src/cli/help.mjs CHANGED
@@ -34,7 +34,7 @@ Commands:
34
34
  task close-merged <dir> [--task <id>] [--narrate-only] [--dry-run] Sweep shipped-pending-merge tasks for confirmed merges (--narrate-only returns pending_narration[] entries read-only without closing).
35
35
  task narration-drain <dir> --task <task-id> Remove a row from state.pending_narration[] under withFileLock (called by /apt:close-task --narrate-only after a successful narrator spawn)
36
36
  task complete-subtask <dir> --task-id <id> --subtask-id <sid|a+b+c> [--commit <sha>] [--notes <text>] Atomic plan ↔ build-progress sync
37
- features-audit <dir> [--diff-files <csv>|--since <ref>] [--apply-stubs] [--format json|md] Audit features registry for unregistered feature-qualifying files
37
+ features-audit <dir> [--task-id <id>] [--no-worktree-redirect] [--diff-files <csv>|--since <ref>] [--apply-stubs] [--format json|md] Audit features registry for unregistered feature-qualifying files (BUG-025b: --task-id redirects writes to active task's worktree)
38
38
  docs-audit <dir> [--diff-files <csv>|--since <ref>|--dry-run] [--policy <path>] [--format json|md] Audit documented surfaces for docs-drift
39
39
 
40
40
  lock claim <dir> --subtask <id> Claim exclusive lock on a subtask
@@ -110,7 +110,9 @@ Commands:
110
110
 
111
111
  migrate-pr-reviews <project-dir> Migrate legacy .aperant/reviews/ → .aperant/pr-reviews/pr-{N}/rounds/{R}/ (idempotent)
112
112
 
113
- validate-evidence <findings-dir> --worktree <path> Enforce the Evidence Rule (literal substring + multi-file check); mutates findings markdown in place
113
+ validate-evidence <findings-dir> --worktree <path> [--allow-approx-evidence] [--approx-floor <n>] [--llm-validator-confirmed] Enforce the Evidence Rule (literal substring + multi-file check); mutates findings markdown in place. BUG-014: --allow-approx-evidence opts into Jaccard + line-anchor fallback (downgrade to dismissed_evidence_approx_confirmed unless --llm-validator-confirmed promotes to confirmed_valid_approx).
114
+
115
+ vitest doctor <project-dir> [--format json|md] BUG-026: walk vitest.config.{ts,js,mjs} files, report NODE_ENV/jsxAutomaticShim matrix, warn when JSX-rendering tests lack jsxAutomaticShim coverage (exits 1 on warnings)
114
116
 
115
117
  wfrun validate <workflow.yaml> C17: parse + schema-check a declarative YAML workflow (exits 0 on success)
116
118
  wfrun plan <workflow.yaml> [--input k=v …] C17: resolve DAG waves + inputs, emit run-plan JSON (no side effects)
@@ -126,6 +128,9 @@ Commands:
126
128
  pr-review record-comment <dir> --pr <N> --round <R> --url <url> --body-hash <sha256> [--review-id <id>] [--posted-at <iso>] Append entry to comment_timeline (Phase 6b)
127
129
  pr-review audit-fixer <dir> --review-dir <path> --iteration <N> --fixer <name> --worktree <path> --status-line <str> Audit fixer output against git state; exits 0 with JSON envelope (verdict: verified|hallucinated|skipped)
128
130
 
131
+ ci-watch start|stop|status|tick <dir> [--pr <N>] [...] /apt:watch-ci primitives — see SKILL.md for full flag matrix
132
+ ci-watch sweep <dir> --orphaned [--dry-run] BUG-027: enumerate ci-watches state files, query gh per PR, remove terminal-state (MERGED|CLOSED) entries (manual GC; rate-limited)
133
+
129
134
  Flags:
130
135
  --version Print version and exit
131
136
  --help Print usage and exit
@@ -33,6 +33,11 @@ import { MANIFEST_FILENAME } from './manifest.mjs'
33
33
  * target mapped to a host CLI (e.g. `pi`,
34
34
  * which has a detectable host but no apt
35
35
  * install root of its own).
36
+ * @property {string} [displayLabel] Optional user-facing label (e.g. "Kilo Code").
37
+ * When unset, callers fall back to the
38
+ * capitalized form of `id`. Use for runtimes
39
+ * whose product branding differs from the
40
+ * internal id (C58: `kilo` → "Kilo Code").
36
41
  */
37
42
 
38
43
  /** @type {RuntimeDescriptor[]} */
@@ -78,6 +83,10 @@ export const RUNTIMES = Object.freeze([
78
83
  installRoot: '.kilo',
79
84
  markers: ['.kilo/config.json'],
80
85
  cliId: 'kilo',
86
+ // C58 (0.7.2): user-visible label. Internal id stays `kilo` (zero
87
+ // migration per spec ID-01). `--kilocode` is also accepted as a
88
+ // deprecated alias of `--kilo` at the CLI flag parser layer.
89
+ displayLabel: 'Kilo Code',
81
90
  },
82
91
  {
83
92
  id: 'copilot',
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
6
6
  import { join } from 'node:path'
7
+ import { loadMergedProjectConfig } from '../config/load.mjs'
7
8
  import { withFileLock } from '../util/fs-lock.mjs'
8
9
  import { formatDateDisplay, formatTimeDisplay } from './format.mjs'
9
10
 
@@ -15,8 +16,20 @@ export function regenerateTaskIndex(targetDir) {
15
16
  const statePath = join(targetDir, '.aperant', 'state.json')
16
17
  if (!existsSync(statePath)) return
17
18
 
18
- const configPath = join(targetDir, '.aperant', 'config.json')
19
- const config = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf-8')) : {}
19
+ // AUDIT-001: read through the merged loader so per-device
20
+ // preferences.date_format / preferences.time_format overrides land
21
+ // even when the shared config.json no longer carries those fields.
22
+ // Guard: malformed config.local.json must not crash every task state
23
+ // transition — fall back to shared-config-only read and warn on stderr.
24
+ let config
25
+ try {
26
+ config = loadMergedProjectConfig(targetDir) ?? {}
27
+ } catch (e) {
28
+ process.stderr.write(
29
+ `[index-md] config.local.json parse error — using shared config only: ${e.message}\n`,
30
+ )
31
+ config = {}
32
+ }
20
33
  if (config.preferences?.task_index === false) return
21
34
 
22
35
  const state = JSON.parse(readFileSync(statePath, 'utf-8'))
@@ -39,7 +39,6 @@
39
39
  "safe_paths": ["**/*.css", "**/*.md", "**/*.test.*", "**/*.spec.*"]
40
40
  },
41
41
  "verification": {
42
- "open_report": true,
43
42
  "report_formats": ["html", "md"],
44
43
  "evidence_capture": true,
45
44
  "max_iterations": 3,
@@ -47,8 +46,7 @@
47
46
  },
48
47
  "multi_model": {
49
48
  "cross_verification": {
50
- "enabled": false,
51
- "tools": []
49
+ "enabled": false
52
50
  },
53
51
  "video_review": {
54
52
  "enabled": false,
@@ -92,14 +90,11 @@
92
90
  "cleanup_on_close": "ask"
93
91
  },
94
92
  "preferences": {
95
- "date_format": "YY-MM-DD",
96
- "time_format": "24h",
97
93
  "task_index": true,
98
94
  "health_check": {
99
95
  "enabled": false,
100
96
  "interval_minutes": 30
101
- },
102
- "quick_task_post_verify": "ask"
97
+ }
103
98
  },
104
99
  "orchestration": {
105
100
  "phase_delegation": "auto",
@@ -21,10 +21,15 @@ OUTPUT FORMAT: verification.json (NOT VERIFICATION.md). JSON is the source of tr
21
21
  <step name="load_config" priority="first">
22
22
  ## 1. Load Config and Context
23
23
 
24
- Read `.aperant/config.json` for verification preferences:
24
+ Read the merged Aperant config for verification preferences (AUDIT-001 — the per-device `verification.open_report` field lives in `.aperant/config.local.json`; the rest in shared `.aperant/config.json`). Use the framework's merged loader so local overrides win:
25
25
  ```bash
26
- cat .aperant/config.json 2>/dev/null || echo "{}"
26
+ node .aperant/deps/node_modules/@aperant/framework/bin/apt-tools.mjs config read . 2>/dev/null || echo "{}"
27
27
  ```
28
+ If `apt-tools config read` is unavailable, fall back to overlaying the two files in JS:
29
+ ```bash
30
+ node -e "const fs=require('fs'); const s=(p)=>{try{return JSON.parse(fs.readFileSync(p,'utf-8'))}catch{return {}}}; function merge(a,b){if(a===null||typeof a!=='object'||Array.isArray(a))return b;if(b===null||typeof b!=='object'||Array.isArray(b))return b;const out={...a};for(const k of Object.keys(b))out[k]=(k in a)?merge(a[k],b[k]):b[k];return out}; const merged=merge(s('.aperant/config.json'),s('.aperant/config.local.json')); console.log(JSON.stringify(merged))"
31
+ ```
32
+ Do NOT read `.aperant/config.json` directly — that misses per-device overrides like `verification.open_report` and `multi_model.cross_verification.tools`.
28
33
 
29
34
  Read project constitution:
30
35
  ```bash
@@ -396,7 +401,7 @@ ln -sf "${PROOF_DIR}/report.html" .aperant/proof-report.html
396
401
  <step name="open_report">
397
402
  ## 11. Open Report
398
403
 
399
- If `config.verification.open_report` is true (default):
404
+ Read `verification.open_report` from the MERGED config (per-device override lives in `.aperant/config.local.json` per AUDIT-001; do not read raw `.aperant/config.json`). If true (default):
400
405
  ```bash
401
406
  open "${PROOF_DIR}/report.html"
402
407
  ```