@aperant/framework 0.7.0 → 0.7.3
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.
- package/CHANGELOG.md +180 -0
- package/agents/apt-planner.md +12 -0
- package/agents/apt-pr-review-fixer.md +13 -9
- package/bin/apt-tools.mjs +7 -0
- package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -1
- package/dist/cli/ci-watch/stop-matrix.mjs +16 -0
- package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -1
- package/dist/cli/commands/ci-watch.d.mts +11 -0
- package/dist/cli/commands/ci-watch.d.mts.map +1 -1
- package/dist/cli/commands/ci-watch.mjs +108 -2
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/features-audit.d.mts +24 -0
- package/dist/cli/commands/features-audit.d.mts.map +1 -1
- package/dist/cli/commands/features-audit.mjs +159 -5
- package/dist/cli/commands/features-audit.mjs.map +1 -1
- package/dist/cli/commands/health-check.d.mts +16 -0
- package/dist/cli/commands/health-check.d.mts.map +1 -1
- package/dist/cli/commands/health-check.mjs +118 -2
- package/dist/cli/commands/health-check.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts +9 -0
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +49 -4
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/commands/modes.d.mts.map +1 -1
- package/dist/cli/commands/modes.mjs +11 -0
- package/dist/cli/commands/modes.mjs.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.d.mts +13 -0
- package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.mjs +18 -5
- package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +84 -5
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/validate-evidence.d.mts +24 -2
- package/dist/cli/commands/validate-evidence.d.mts.map +1 -1
- package/dist/cli/commands/validate-evidence.mjs +154 -17
- package/dist/cli/commands/validate-evidence.mjs.map +1 -1
- package/dist/cli/commands/vitest-doctor.d.mts +2 -0
- package/dist/cli/commands/vitest-doctor.d.mts.map +1 -0
- package/dist/cli/commands/vitest-doctor.mjs +168 -0
- package/dist/cli/commands/vitest-doctor.mjs.map +1 -0
- package/dist/cli/config/gitignore-drift.d.mts +23 -1
- package/dist/cli/config/gitignore-drift.d.mts.map +1 -1
- package/dist/cli/config/gitignore-drift.mjs +81 -3
- package/dist/cli/config/gitignore-drift.mjs.map +1 -1
- package/dist/cli/config/load.d.mts +56 -2
- package/dist/cli/config/load.d.mts.map +1 -1
- package/dist/cli/config/load.mjs +192 -2
- package/dist/cli/config/load.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +7 -0
- package/dist/cli/consistency/parse-review.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +24 -2
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/gate/gates/gitignore-in-sync.d.mts +1 -1
- package/dist/cli/gate/gates/gitignore-in-sync.d.mts.map +1 -1
- package/dist/cli/gate/gates/gitignore-in-sync.mjs +5 -2
- package/dist/cli/gate/gates/gitignore-in-sync.mjs.map +1 -1
- package/dist/cli/gate/gates/review-clean.d.mts +5 -1
- package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
- package/dist/cli/gate/gates/review-clean.mjs +23 -18
- package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
- package/dist/cli/gate/gates/verify-approved.d.mts +49 -1
- package/dist/cli/gate/gates/verify-approved.d.mts.map +1 -1
- package/dist/cli/gate/gates/verify-approved.mjs +93 -14
- package/dist/cli/gate/gates/verify-approved.mjs.map +1 -1
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +7 -2
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/install/runtime-detect.d.mts +13 -0
- package/dist/cli/install/runtime-detect.d.mts.map +1 -1
- package/dist/cli/install/runtime-detect.mjs +9 -0
- package/dist/cli/install/runtime-detect.mjs.map +1 -1
- package/dist/cli/task/index-md.d.mts.map +1 -1
- package/dist/cli/task/index-md.mjs +14 -2
- package/dist/cli/task/index-md.mjs.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +11 -2
- package/dist/plugin/agents/apt-improver.md +99 -0
- package/dist/plugin/agents/apt-planner.md +127 -10
- package/dist/plugin/agents/apt-pr-review-fixer.md +13 -9
- package/dist/plugin/skills/apt/SKILL.md +1 -0
- package/dist/plugin/skills/apt-close-task/SKILL.md +63 -1
- package/dist/plugin/skills/apt-debug/SKILL.md +39 -6
- package/dist/plugin/skills/apt-debug/appendices/diagnose-discipline.md +119 -0
- package/dist/plugin/skills/apt-diagram/SKILL.md +378 -0
- package/dist/plugin/skills/apt-diagram/appendices/design-discipline.md +97 -0
- package/dist/plugin/skills/apt-discuss/SKILL.md +72 -5
- package/dist/plugin/skills/apt-discuss/appendices/grill-discipline.md +104 -0
- package/dist/plugin/skills/apt-discuss/appendices/zoom-out-helper.md +79 -0
- package/dist/plugin/skills/apt-execute/SKILL.md +57 -5
- package/dist/plugin/skills/apt-execute/appendices/tdd-mode.md +107 -0
- package/dist/plugin/skills/apt-improve/DEEPENING.md +84 -0
- package/dist/plugin/skills/apt-improve/INTERFACE-DESIGN.md +97 -0
- package/dist/plugin/skills/apt-improve/LANGUAGE.md +104 -0
- package/dist/plugin/skills/apt-improve/SKILL.md +141 -0
- package/dist/plugin/skills/apt-plan/SKILL.md +171 -4
- package/dist/plugin/skills/apt-plan/adapters/conductor.md +98 -0
- package/dist/plugin/skills/apt-pr-review/SKILL.md +57 -18
- package/dist/plugin/skills/apt-prototype/LOGIC.md +109 -0
- package/dist/plugin/skills/apt-prototype/SKILL.md +143 -0
- package/dist/plugin/skills/apt-prototype/UI.md +90 -0
- package/dist/plugin/skills/apt-quick/SKILL.md +49 -8
- package/dist/plugin/skills/apt-release-notes/SKILL.md +193 -0
- package/dist/plugin/skills/apt-release-notes/appendices/persona-voice.md +59 -0
- package/dist/plugin/skills/apt-review/SKILL.md +2 -0
- package/dist/plugin/skills/apt-run/SKILL.md +32 -4
- package/dist/plugin/skills/apt-setup/SKILL.md +308 -6
- package/dist/plugin/skills/apt-ship/SKILL.md +122 -1
- package/dist/plugin/skills/apt-spar/SKILL.md +315 -0
- package/dist/plugin/skills/apt-triage/AGENT-BRIEF.md +84 -0
- package/dist/plugin/skills/apt-triage/OUT-OF-SCOPE.md +75 -0
- package/dist/plugin/skills/apt-triage/SKILL.md +169 -0
- package/dist/plugin/skills/apt-update/SKILL.md +77 -10
- package/dist/plugin/skills/apt-verify/SKILL.md +3 -0
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +10 -5
- package/dist/plugin/skills/apt-watch-ci/SKILL.md +166 -0
- package/dist/plugin/skills/apt-zoom-out/SKILL.md +130 -0
- package/package.json +133 -133
- package/prompts/conductor-framework-context.md +63 -0
- package/prompts/conductor-system.md +11 -0
- package/skills/apt-close-task/SKILL.md +1 -0
- package/skills/apt-discuss/SKILL.md +47 -5
- package/skills/apt-execute/SKILL.md +9 -0
- package/skills/apt-plan/SKILL.md +12 -0
- package/skills/apt-pr-review/SKILL.md +11 -2
- package/skills/apt-quick/SKILL.md +19 -8
- package/skills/apt-researcher.md +1 -0
- package/skills/apt-setup/SKILL.md +33 -2
- package/skills/apt-verify-proof/SKILL.md +7 -5
- package/src/cli/ci-watch/stop-matrix.mjs +17 -0
- package/src/cli/commands/ci-watch.mjs +113 -2
- package/src/cli/commands/features-audit.mjs +164 -5
- package/src/cli/commands/health-check.mjs +115 -2
- package/src/cli/commands/init.mjs +52 -4
- package/src/cli/commands/modes.mjs +11 -0
- package/src/cli/commands/pr-review-audit-fixer.mjs +18 -5
- package/src/cli/commands/task.mjs +83 -5
- package/src/cli/commands/validate-evidence.mjs +158 -17
- package/src/cli/commands/vitest-doctor.mjs +173 -0
- package/src/cli/config/gitignore-drift.mjs +74 -3
- package/src/cli/config/load.mjs +188 -2
- package/src/cli/consistency/parse-review.mjs +6 -0
- package/src/cli/dispatch.mjs +23 -2
- package/src/cli/gate/gates/gitignore-in-sync.mjs +5 -2
- package/src/cli/gate/gates/review-clean.mjs +24 -19
- package/src/cli/gate/gates/verify-approved.mjs +97 -14
- package/src/cli/help.mjs +7 -2
- package/src/cli/install/runtime-detect.mjs +9 -0
- package/src/cli/task/index-md.mjs +15 -2
- package/templates/config.json +2 -7
- package/workflows/verify-proof.md +8 -3
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { existsSync, readFileSync } from 'node:fs'
|
|
16
16
|
import { join } from 'node:path'
|
|
17
|
+
import { detectWorktree } from '../git/worktree-detect.mjs'
|
|
17
18
|
import { resolveSharePolicy } from './share-policy.mjs'
|
|
18
19
|
import { buildAperantGitignoreTemplate } from './upgrade-gitignore.mjs'
|
|
19
20
|
|
|
@@ -35,11 +36,66 @@ const SKIP_COMMANDS = new Set([
|
|
|
35
36
|
'-h',
|
|
36
37
|
])
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* BUG-028 — resolve the shared (main-repo) root for the dispatch hook.
|
|
41
|
+
*
|
|
42
|
+
* Mirrors `coordination/agent-identity.mjs::resolveSharedRoot` but does NOT
|
|
43
|
+
* depend on that module. The CoordinationStore seam is `@internal` (R8) —
|
|
44
|
+
* `bin/apt-tools.mjs` and its dispatch hooks must reach git through
|
|
45
|
+
* `git/worktree-detect.mjs` directly, not through `coordination/*`.
|
|
46
|
+
*
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
function resolveSharedRootForDrift(projectDir) {
|
|
50
|
+
try {
|
|
51
|
+
const wt = detectWorktree(projectDir)
|
|
52
|
+
return wt.is_worktree && wt.main_repo_root ? wt.main_repo_root : projectDir
|
|
53
|
+
} catch {
|
|
54
|
+
return projectDir
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* BUG-028 — pretty-print the first few missing / extra lines as a
|
|
60
|
+
* unified-diff snippet for the gate's `reason` field. Capped at
|
|
61
|
+
* `maxLines` per side so the reason stays short enough for terminal
|
|
62
|
+
* output. Returns the empty string when both sides match.
|
|
63
|
+
*/
|
|
64
|
+
export function buildGitignoreDiffSummary(expected, actual, maxLines = 3) {
|
|
65
|
+
const expSet = new Set(String(expected).split('\n'))
|
|
66
|
+
const actSet = new Set(String(actual).split('\n'))
|
|
67
|
+
const missing = []
|
|
68
|
+
for (const line of expSet) {
|
|
69
|
+
if (line === '') continue
|
|
70
|
+
if (!actSet.has(line)) {
|
|
71
|
+
missing.push(line)
|
|
72
|
+
if (missing.length >= maxLines) break
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const extra = []
|
|
76
|
+
for (const line of actSet) {
|
|
77
|
+
if (line === '') continue
|
|
78
|
+
if (!expSet.has(line)) {
|
|
79
|
+
extra.push(line)
|
|
80
|
+
if (extra.length >= maxLines) break
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (missing.length === 0 && extra.length === 0) return ''
|
|
84
|
+
const parts = []
|
|
85
|
+
for (const line of missing) parts.push(`+ ${line}`)
|
|
86
|
+
for (const line of extra) parts.push(`- ${line}`)
|
|
87
|
+
return parts.join('\n')
|
|
88
|
+
}
|
|
89
|
+
|
|
38
90
|
/**
|
|
39
91
|
* Pure comparison primitive — returns whether the on-disk `.aperant/.gitignore`
|
|
40
92
|
* differs from the policy-expected template. Returns `{ drifted: false }` when
|
|
41
93
|
* the `.aperant/` dir or file is absent (nothing to compare).
|
|
42
94
|
*
|
|
95
|
+
* BUG-028: when drifted, embeds a unified-diff summary (≤3 missing/extra
|
|
96
|
+
* lines per side) in `reason` so the user sees exactly what drifted
|
|
97
|
+
* without re-running `apt-tools config upgrade-gitignore --dry-run`.
|
|
98
|
+
*
|
|
43
99
|
* @internal
|
|
44
100
|
*/
|
|
45
101
|
export function checkGitignoreDrift(projectRoot) {
|
|
@@ -65,7 +121,16 @@ export function checkGitignoreDrift(projectRoot) {
|
|
|
65
121
|
} catch {
|
|
66
122
|
return { drifted: false, reason: 'read-failed' }
|
|
67
123
|
}
|
|
68
|
-
|
|
124
|
+
const drifted = actual !== expected
|
|
125
|
+
if (!drifted) return { drifted: false, reason: null }
|
|
126
|
+
const diff = buildGitignoreDiffSummary(expected, actual)
|
|
127
|
+
return {
|
|
128
|
+
drifted: true,
|
|
129
|
+
reason: diff
|
|
130
|
+
? `.aperant/.gitignore drift detected:\n${diff}`
|
|
131
|
+
: '.aperant/.gitignore drift detected (whitespace-only)',
|
|
132
|
+
diff,
|
|
133
|
+
}
|
|
69
134
|
}
|
|
70
135
|
|
|
71
136
|
/**
|
|
@@ -74,15 +139,21 @@ export function checkGitignoreDrift(projectRoot) {
|
|
|
74
139
|
* - `command` is not in the hot-path allowlist
|
|
75
140
|
* - `.aperant/.gitignore` exists AND does not match the expected template
|
|
76
141
|
*
|
|
142
|
+
* BUG-028: resolves the shared (main-repo) root internally so the same drift
|
|
143
|
+
* case produces the same verdict regardless of cwd context. Pre-fix this
|
|
144
|
+
* inspected `cwd` directly, producing a warning from one cwd context and a
|
|
145
|
+
* clean envelope from another for the same on-disk drift.
|
|
146
|
+
*
|
|
77
147
|
* Never throws.
|
|
78
148
|
*
|
|
79
149
|
* @internal
|
|
80
150
|
*/
|
|
81
|
-
export function emitDriftWarningIfAny(
|
|
151
|
+
export function emitDriftWarningIfAny(cwd, command) {
|
|
82
152
|
try {
|
|
83
153
|
if (process.env.APT_SUPPRESS_GITIGNORE_DRIFT === '1') return
|
|
84
154
|
if (!command || SKIP_COMMANDS.has(command)) return
|
|
85
|
-
const
|
|
155
|
+
const sharedRoot = resolveSharedRootForDrift(cwd)
|
|
156
|
+
const { drifted } = checkGitignoreDrift(sharedRoot)
|
|
86
157
|
if (drifted) {
|
|
87
158
|
process.stderr.write(
|
|
88
159
|
'[apt-tools] warning: .aperant/.gitignore is out of sync with share policy — run `apt-tools config upgrade-gitignore . --dry-run` to see the diff.\n',
|
package/src/cli/config/load.mjs
CHANGED
|
@@ -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 {
|
|
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
|
}
|
package/src/cli/dispatch.mjs
CHANGED
|
@@ -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) =>
|
|
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: {
|
|
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,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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/
|
|
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 ??
|
|
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/
|
|
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"
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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:
|
|
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
|
-
|
|
45
|
-
|
|
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: '
|
|
48
|
-
reason:
|
|
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: '
|
|
55
|
-
reason:
|
|
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
|