@aperant/framework 0.6.7 → 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 +240 -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 +147 -3
- 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 +119 -3
- package/dist/cli/commands/health-check.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts +19 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +143 -8
- 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/route.d.mts.map +1 -1
- package/dist/cli/commands/route.mjs +37 -2
- package/dist/cli/commands/route.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +132 -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 +8 -2
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/host/detect.d.mts +122 -9
- package/dist/cli/host/detect.d.mts.map +1 -1
- package/dist/cli/host/detect.mjs +132 -0
- package/dist/cli/host/detect.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts +38 -0
- package/dist/cli/install/legacy-paths.d.mts.map +1 -0
- package/dist/cli/install/legacy-paths.mjs +69 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -0
- 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/install/runtime-migrate.d.mts +84 -0
- package/dist/cli/install/runtime-migrate.d.mts.map +1 -0
- package/dist/cli/install/runtime-migrate.mjs +244 -0
- package/dist/cli/install/runtime-migrate.mjs.map +1 -0
- package/dist/cli/route/drift-detect.d.mts +20 -0
- package/dist/cli/route/drift-detect.d.mts.map +1 -0
- package/dist/cli/route/drift-detect.mjs +107 -0
- package/dist/cli/route/drift-detect.mjs.map +1 -0
- 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/cli/util/aperant-section.d.mts +34 -0
- package/dist/cli/util/aperant-section.d.mts.map +1 -0
- package/dist/cli/util/aperant-section.mjs +127 -0
- package/dist/cli/util/aperant-section.mjs.map +1 -0
- package/dist/cli/util/copy.d.mts +28 -1
- package/dist/cli/util/copy.d.mts.map +1 -1
- package/dist/cli/util/copy.mjs +43 -55
- package/dist/cli/util/copy.mjs.map +1 -1
- package/dist/cli/util/semver.d.mts +17 -0
- package/dist/cli/util/semver.d.mts.map +1 -0
- package/dist/cli/util/semver.mjs +29 -0
- package/dist/cli/util/semver.mjs.map +1 -0
- package/dist/cli/util/skill-installs.d.mts +65 -9
- package/dist/cli/util/skill-installs.d.mts.map +1 -1
- package/dist/cli/util/skill-installs.mjs +130 -21
- package/dist/cli/util/skill-installs.mjs.map +1 -1
- package/dist/cli/util/version-preflight.d.mts +44 -0
- package/dist/cli/util/version-preflight.d.mts.map +1 -0
- package/dist/cli/util/version-preflight.mjs +66 -0
- package/dist/cli/util/version-preflight.mjs.map +1 -0
- 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 +6 -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-ship/SKILL.md +16 -4
- package/skills/apt-spar/SKILL.md +36 -11
- package/skills/apt-update/SKILL.md +26 -1
- package/skills/apt-verify-proof/SKILL.md +7 -5
- package/skills/apt-watch-ci/SKILL.md +4 -1
- package/src/cli/ci-watch/stop-matrix.mjs +17 -0
- package/src/cli/commands/ci-watch.mjs +152 -3
- package/src/cli/commands/features-audit.mjs +164 -5
- package/src/cli/commands/health-check.mjs +116 -3
- package/src/cli/commands/init.mjs +154 -6
- package/src/cli/commands/modes.mjs +11 -0
- package/src/cli/commands/pr-review-audit-fixer.mjs +18 -5
- package/src/cli/commands/route.mjs +38 -2
- package/src/cli/commands/task.mjs +132 -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 +8 -2
- package/src/cli/host/detect.mjs +135 -0
- package/src/cli/install/legacy-paths.mjs +69 -0
- package/src/cli/install/runtime-detect.mjs +9 -0
- package/src/cli/install/runtime-migrate.mjs +252 -0
- package/src/cli/route/drift-detect.mjs +107 -0
- package/src/cli/task/index-md.mjs +15 -2
- package/src/cli/util/aperant-section.mjs +136 -0
- package/src/cli/util/copy.mjs +43 -56
- package/src/cli/util/semver.mjs +28 -0
- package/src/cli/util/skill-installs.mjs +134 -21
- package/src/cli/util/version-preflight.mjs +65 -0
- package/templates/aperant-claude-md-appendix.md +37 -0
- package/templates/config.json +2 -7
- package/workflows/verify-proof.md +8 -3
|
@@ -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
|
@@ -31,9 +31,10 @@ Commands:
|
|
|
31
31
|
task update <dir> --id <id> [--lifecycle-phase <p>] [--pr-url <url>] [--subtasks-total <N>] [--depends-on <refs>] Update non-linkage task state; --pr-url may arm ci-watch per ci_watch.after_ship
|
|
32
32
|
task move <dir> --id <id> [--to-scope <s>] [--to-milestone <id>] [--to-phase <id>] [--to-roadmap-ref-url <url>] [--reason <text>] [--force] Reassign scope/milestone/phase with history
|
|
33
33
|
task close <dir> --id <task-id> [--verdict <v>] Close task and move to completed
|
|
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).
|
|
34
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)
|
|
35
36
|
task complete-subtask <dir> --task-id <id> --subtask-id <sid|a+b+c> [--commit <sha>] [--notes <text>] Atomic plan ↔ build-progress sync
|
|
36
|
-
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)
|
|
37
38
|
docs-audit <dir> [--diff-files <csv>|--since <ref>|--dry-run] [--policy <path>] [--format json|md] Audit documented surfaces for docs-drift
|
|
38
39
|
|
|
39
40
|
lock claim <dir> --subtask <id> Claim exclusive lock on a subtask
|
|
@@ -109,7 +110,9 @@ Commands:
|
|
|
109
110
|
|
|
110
111
|
migrate-pr-reviews <project-dir> Migrate legacy .aperant/reviews/ → .aperant/pr-reviews/pr-{N}/rounds/{R}/ (idempotent)
|
|
111
112
|
|
|
112
|
-
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)
|
|
113
116
|
|
|
114
117
|
wfrun validate <workflow.yaml> C17: parse + schema-check a declarative YAML workflow (exits 0 on success)
|
|
115
118
|
wfrun plan <workflow.yaml> [--input k=v …] C17: resolve DAG waves + inputs, emit run-plan JSON (no side effects)
|
|
@@ -125,6 +128,9 @@ Commands:
|
|
|
125
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)
|
|
126
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)
|
|
127
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
|
+
|
|
128
134
|
Flags:
|
|
129
135
|
--version Print version and exit
|
|
130
136
|
--help Print usage and exit
|
package/src/cli/host/detect.mjs
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* for misses over silently routing into the wrong runtime.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { existsSync as fsExistsSync } from 'node:fs'
|
|
16
|
+
import { delimiter as PATH_DELIMITER, join as pathJoin } from 'node:path'
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* Canonical list of runtime identifiers accepted by detectHost's
|
|
17
20
|
* `overrideCli` parameter AND by the route command's `--runtime <id>` flag.
|
|
@@ -211,5 +214,137 @@ export function detectHost(opts = {}) {
|
|
|
211
214
|
capabilities,
|
|
212
215
|
aperant_terminal: isAperantTerminal,
|
|
213
216
|
aperant_socket: aperantSocket,
|
|
217
|
+
partner_transports: detectPartnerTransports({ env }),
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Partner-transport ladder used by `apt:spar` (FRAMEWORK-BUG-024).
|
|
223
|
+
*
|
|
224
|
+
* For each partner (codex / claude / gemini) we resolve a 3-rung ladder in
|
|
225
|
+
* preference order — MCP first (cleanest integration), then plugin shim
|
|
226
|
+
* (codex only), then raw CLI on $PATH. The first rung with `detected: true`
|
|
227
|
+
* wins; the spar skill's gate fails only when all three rungs report
|
|
228
|
+
* `detected: false`.
|
|
229
|
+
*
|
|
230
|
+
* Detection is best-effort and side-effect-free w/r/t the FS — only
|
|
231
|
+
* `existsSync` reads. No spawn, no network. The MCP rung uses
|
|
232
|
+
* `detected: "runtime-host-only"` when the framework process cannot probe
|
|
233
|
+
* the host's deferred-tool registry (the common case — MCP availability
|
|
234
|
+
* is host-internal), signalling the SKILL body to self-check at invocation
|
|
235
|
+
* time rather than fail the gate.
|
|
236
|
+
*
|
|
237
|
+
* Pure function w/r/t injected `env` + `existsSync` so tests can mock both
|
|
238
|
+
* without monkey-patching `process.env` or `node:fs`.
|
|
239
|
+
*
|
|
240
|
+
* @param {object} [opts]
|
|
241
|
+
* @param {NodeJS.ProcessEnv} [opts.env] — defaults to `process.env`
|
|
242
|
+
* @param {(p: string) => boolean} [opts.existsSync] — defaults to `node:fs` existsSync
|
|
243
|
+
*/
|
|
244
|
+
export function detectPartnerTransports(opts = {}) {
|
|
245
|
+
const env = opts.env ?? process.env
|
|
246
|
+
const exists = opts.existsSync ?? fsExistsSync
|
|
247
|
+
const pathEntries = (env.PATH ?? '').split(PATH_DELIMITER).filter(Boolean)
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
codex: [
|
|
251
|
+
detectMcpRung('codex', env),
|
|
252
|
+
detectPluginShim('codex', env, exists),
|
|
253
|
+
detectRawCli('codex', pathEntries, exists),
|
|
254
|
+
],
|
|
255
|
+
claude: [
|
|
256
|
+
detectMcpRung('claude', env),
|
|
257
|
+
// No plugin shim exists for Claude today — kept as `detected: false`
|
|
258
|
+
// for ladder-shape symmetry so consumers can index by rung uniformly.
|
|
259
|
+
{ rung: 'plugin', detected: false, reason: 'no plugin shim for claude' },
|
|
260
|
+
detectRawCli('claude', pathEntries, exists),
|
|
261
|
+
],
|
|
262
|
+
gemini: [
|
|
263
|
+
detectMcpRung('gemini', env),
|
|
264
|
+
{ rung: 'plugin', detected: false, reason: 'no plugin shim for gemini' },
|
|
265
|
+
detectRawCli('gemini', pathEntries, exists),
|
|
266
|
+
],
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* MCP rung — detected if the host exposes its deferred-tool manifest via
|
|
272
|
+
* `CLAUDE_MCP_DEFERRED_TOOLS` (a JSON array of tool names) AND a matching
|
|
273
|
+
* `mcp__<partner>-mcp__*` entry is present. When the env var is absent the
|
|
274
|
+
* framework cannot observe the host's tool registry from outside, so we
|
|
275
|
+
* return `detected: "runtime-host-only"` — the SKILL body interprets this
|
|
276
|
+
* as "try it; if the host has the tool, use it; otherwise fall through".
|
|
277
|
+
*/
|
|
278
|
+
function detectMcpRung(partner, env) {
|
|
279
|
+
const toolPrefix = mcpToolPrefix(partner)
|
|
280
|
+
const manifest = env.CLAUDE_MCP_DEFERRED_TOOLS
|
|
281
|
+
if (manifest) {
|
|
282
|
+
try {
|
|
283
|
+
const tools = JSON.parse(manifest)
|
|
284
|
+
if (
|
|
285
|
+
Array.isArray(tools) &&
|
|
286
|
+
tools.some((t) => typeof t === 'string' && t.startsWith(toolPrefix))
|
|
287
|
+
) {
|
|
288
|
+
return { rung: 'mcp', detected: true, invocation: `${toolPrefix}*` }
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
rung: 'mcp',
|
|
292
|
+
detected: false,
|
|
293
|
+
reason: `no ${toolPrefix}* tool in CLAUDE_MCP_DEFERRED_TOOLS`,
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
// Malformed manifest — fall through to runtime-deferred
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { rung: 'mcp', detected: 'runtime-host-only', invocation: `${toolPrefix}*` }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function mcpToolPrefix(partner) {
|
|
303
|
+
if (partner === 'codex') return 'mcp__codex-mcp__'
|
|
304
|
+
if (partner === 'claude') return 'mcp__claude-code-mcp__'
|
|
305
|
+
if (partner === 'gemini') return 'mcp__gemini-mcp__'
|
|
306
|
+
return `mcp__${partner}-mcp__`
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Plugin-shim rung — Claude Code plugins drop helper scripts under
|
|
311
|
+
* `${CLAUDE_PLUGIN_ROOT}/scripts/<partner>-companion.mjs`. Only codex
|
|
312
|
+
* ships a companion today; the caller passes `'codex'` to probe.
|
|
313
|
+
*/
|
|
314
|
+
function detectPluginShim(partner, env, exists) {
|
|
315
|
+
const root = env.CLAUDE_PLUGIN_ROOT
|
|
316
|
+
if (!root) {
|
|
317
|
+
return { rung: 'plugin', detected: false, reason: 'CLAUDE_PLUGIN_ROOT unset' }
|
|
318
|
+
}
|
|
319
|
+
const script = pathJoin(root, 'scripts', `${partner}-companion.mjs`)
|
|
320
|
+
if (!exists(script)) {
|
|
321
|
+
return { rung: 'plugin', detected: false, reason: `${script} not found` }
|
|
214
322
|
}
|
|
323
|
+
return { rung: 'plugin', detected: true, invocation: `node "${script}" task` }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Raw-CLI rung — probe pre-split $PATH entries for the partner binary using
|
|
328
|
+
* existsSync (no spawn). Accepts a pre-split array so detectPartnerTransports
|
|
329
|
+
* can parse $PATH once and share it across all partner probes.
|
|
330
|
+
*
|
|
331
|
+
* The binary name matches the partner id for all three currently-supported
|
|
332
|
+
* partners (codex / claude / gemini).
|
|
333
|
+
*/
|
|
334
|
+
function detectRawCli(partner, pathEntries, exists) {
|
|
335
|
+
const bin = partner
|
|
336
|
+
for (const dir of pathEntries) {
|
|
337
|
+
const candidate = pathJoin(dir, bin)
|
|
338
|
+
if (exists(candidate)) {
|
|
339
|
+
return { rung: 'cli', detected: true, invocation: rawCliInvocation(partner) }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return { rung: 'cli', detected: false, reason: `${bin} not found on $PATH` }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function rawCliInvocation(partner) {
|
|
346
|
+
if (partner === 'codex') return 'codex exec'
|
|
347
|
+
if (partner === 'claude') return 'claude -p'
|
|
348
|
+
if (partner === 'gemini') return 'gemini -m'
|
|
349
|
+
return partner
|
|
215
350
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install/legacy-paths.mjs — shared registry of legacy install topologies
|
|
3
|
+
* the framework once wrote to but no longer owns. Single source of truth
|
|
4
|
+
* for both:
|
|
5
|
+
*
|
|
6
|
+
* - BUG-021: `runLegacyCleanup` in `install/runtime-migrate.mjs` knows
|
|
7
|
+
* which files under a legacy root are framework-owned (safe to
|
|
8
|
+
* remove) vs user-added (preserve).
|
|
9
|
+
* - BUG-022: `readSkillInstallState` in `util/skill-installs.mjs` falls
|
|
10
|
+
* back to `version_file` here when no manifest is present, so a
|
|
11
|
+
* pre-manifest install still surfaces as "installed" rather than
|
|
12
|
+
* "missing-skills".
|
|
13
|
+
*
|
|
14
|
+
* Derivation: the codex `framework_files` list was extracted from
|
|
15
|
+
* `git show 1a8dd8a8 --stat` (the original "make Codex a first-class
|
|
16
|
+
* skill install surface" commit) — every `.agents/skills/…` path the
|
|
17
|
+
* installer once wrote, including the `.apt-version` stamp and the
|
|
18
|
+
* apt-pr-review / apt-discuss / apt-verify-proof sub-files. When the
|
|
19
|
+
* codex installer was retargeted to `.codex/`, that entire tree became
|
|
20
|
+
* orphaned on upgrading projects.
|
|
21
|
+
*
|
|
22
|
+
* Schema:
|
|
23
|
+
* { [runtimeId]: {
|
|
24
|
+
* install_root: relative path the runtime once wrote into,
|
|
25
|
+
* version_file: relative path of the legacy `.apt-version` stamp,
|
|
26
|
+
* framework_files: array of relative paths the installer once owned
|
|
27
|
+
* (under install_root).
|
|
28
|
+
* } }
|
|
29
|
+
*
|
|
30
|
+
* Frozen via Object.freeze so callers cannot mutate the shared registry.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** @type {Record<string, { install_root: string, version_file: string, framework_files: ReadonlyArray<string> }>} */
|
|
34
|
+
export const LEGACY_INSTALL_PATHS = Object.freeze({
|
|
35
|
+
codex: Object.freeze({
|
|
36
|
+
install_root: '.agents/skills',
|
|
37
|
+
version_file: '.agents/skills/.apt-version',
|
|
38
|
+
framework_files: Object.freeze([
|
|
39
|
+
'.apt-version',
|
|
40
|
+
'apt/SKILL.md',
|
|
41
|
+
'apt-debug/SKILL.md',
|
|
42
|
+
'apt-discuss/SKILL.md',
|
|
43
|
+
'apt-discuss/examples/rationalization-example.md',
|
|
44
|
+
'apt-docs/SKILL.md',
|
|
45
|
+
'apt-execute/SKILL.md',
|
|
46
|
+
'apt-pause/SKILL.md',
|
|
47
|
+
'apt-plan/SKILL.md',
|
|
48
|
+
'apt-pr-review/SCHEMA.md',
|
|
49
|
+
'apt-pr-review/SKILL.md',
|
|
50
|
+
'apt-pr-review/prompts/fp-gate-codex.md',
|
|
51
|
+
'apt-pr-review/templates/external-comment.md',
|
|
52
|
+
'apt-quick/SKILL.md',
|
|
53
|
+
'apt-resume/SKILL.md',
|
|
54
|
+
'apt-review/SKILL.md',
|
|
55
|
+
'apt-roadmap/SKILL.md',
|
|
56
|
+
'apt-roundtable/SKILL.md',
|
|
57
|
+
'apt-run/SKILL.md',
|
|
58
|
+
'apt-scan/SKILL.md',
|
|
59
|
+
'apt-setup/SKILL.md',
|
|
60
|
+
'apt-ship/SKILL.md',
|
|
61
|
+
'apt-stress-test/SKILL.md',
|
|
62
|
+
'apt-verify/SKILL.md',
|
|
63
|
+
'apt-verify-proof/SKILL.md',
|
|
64
|
+
'apt-verify-proof/references/multi-model-setup.md',
|
|
65
|
+
'apt-verify-proof/references/multi-model-verification.md',
|
|
66
|
+
'apt-verify-proof/references/proof-capture.md',
|
|
67
|
+
]),
|
|
68
|
+
}),
|
|
69
|
+
})
|
|
@@ -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',
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install/runtime-migrate.mjs — at-install cleanup of legacy install
|
|
3
|
+
* trees the framework no longer owns (FRAMEWORK-BUG-021).
|
|
4
|
+
*
|
|
5
|
+
* Codex was originally installed into `.agents/skills/` (commit 1a8dd8a8)
|
|
6
|
+
* and later moved to `.codex/`. Projects that upgraded across that move
|
|
7
|
+
* end up with a stale `.agents/skills/` tree that:
|
|
8
|
+
*
|
|
9
|
+
* - keeps `.apt-version` on disk → the legacy fallback in
|
|
10
|
+
* `readSkillInstallState` reports a stale codex install.
|
|
11
|
+
* - shows up in `health-check` as `stale-skills`.
|
|
12
|
+
* - leaves dozens of orphaned SKILL.md files the runtime no longer
|
|
13
|
+
* reads.
|
|
14
|
+
*
|
|
15
|
+
* `runLegacyCleanup` runs once per `apt-tools init` when codex is in the
|
|
16
|
+
* requested runtime set. It is conservative by default:
|
|
17
|
+
*
|
|
18
|
+
* - Only files listed in `LEGACY_INSTALL_PATHS.codex.framework_files`
|
|
19
|
+
* are removed. User-added skill directories (`my-skill/`, …) are
|
|
20
|
+
* preserved. If anything beyond the known framework set remains,
|
|
21
|
+
* `retained[]` lists those paths.
|
|
22
|
+
* - A present-and-current `apt-file-manifest.json` is treated as the
|
|
23
|
+
* authoritative "this is a managed install, leave it alone" signal:
|
|
24
|
+
* the function no-ops with `reason: 'managed'`.
|
|
25
|
+
* - The caller can suppress cleanup entirely via `--keep-legacy` at the
|
|
26
|
+
* init.mjs site; the function itself is dumb about flags.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { existsSync, readdirSync, rmdirSync, unlinkSync } from 'node:fs'
|
|
30
|
+
import { dirname, join } from 'node:path'
|
|
31
|
+
import { LEGACY_INSTALL_PATHS } from './legacy-paths.mjs'
|
|
32
|
+
import { MANIFEST_FILENAME } from './manifest.mjs'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} LegacyCodexDetection
|
|
36
|
+
* @property {boolean} present True when `.agents/skills/` exists.
|
|
37
|
+
* @property {string[]} frameworkFiles Subset of `framework_files` that
|
|
38
|
+
* actually exist on disk (relative
|
|
39
|
+
* to install_root).
|
|
40
|
+
* @property {string[]} userFiles Everything else under
|
|
41
|
+
* install_root (relative to
|
|
42
|
+
* install_root). May include both
|
|
43
|
+
* files and directories. Drives the
|
|
44
|
+
* `retained[]` warning.
|
|
45
|
+
* @property {boolean} hasAptVersion Whether the legacy `.apt-version`
|
|
46
|
+
* stamp is present.
|
|
47
|
+
* @property {boolean} hasManifest Whether an
|
|
48
|
+
* `apt-file-manifest.json` is
|
|
49
|
+
* present at install_root (signals
|
|
50
|
+
* a managed install — cleanup
|
|
51
|
+
* should no-op).
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
const LEGACY = LEGACY_INSTALL_PATHS.codex
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Recursively enumerate every file path under `dir`, returning paths
|
|
58
|
+
* relative to `dir`. Used to compute the "user files" set (anything not
|
|
59
|
+
* in the framework_files list).
|
|
60
|
+
*
|
|
61
|
+
* Returns [] when the dir doesn't exist. Swallows per-entry errors so a
|
|
62
|
+
* single unreadable symlink doesn't abort detection.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} dir
|
|
65
|
+
* @returns {string[]}
|
|
66
|
+
*/
|
|
67
|
+
function walkRelative(dir) {
|
|
68
|
+
/** @type {string[]} */
|
|
69
|
+
const out = []
|
|
70
|
+
if (!existsSync(dir)) return out
|
|
71
|
+
/** @param {string} current @param {string} rel */
|
|
72
|
+
const visit = (current, rel) => {
|
|
73
|
+
let entries
|
|
74
|
+
try {
|
|
75
|
+
entries = readdirSync(current, { withFileTypes: true })
|
|
76
|
+
} catch {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const childPath = join(current, entry.name)
|
|
81
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name
|
|
82
|
+
if (entry.isDirectory()) {
|
|
83
|
+
visit(childPath, childRel)
|
|
84
|
+
} else if (entry.isFile()) {
|
|
85
|
+
out.push(childRel)
|
|
86
|
+
}
|
|
87
|
+
// Symlinks / other entry types are intentionally ignored — the
|
|
88
|
+
// framework only ever writes regular files.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
visit(dir, '')
|
|
92
|
+
return out
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Probe the codex legacy install tree at `<targetDir>/.agents/skills/`.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} targetDir
|
|
99
|
+
* @returns {LegacyCodexDetection}
|
|
100
|
+
*/
|
|
101
|
+
export function detectLegacyCodexTree(targetDir) {
|
|
102
|
+
const installRoot = join(targetDir, LEGACY.install_root)
|
|
103
|
+
if (!existsSync(installRoot)) {
|
|
104
|
+
return {
|
|
105
|
+
present: false,
|
|
106
|
+
frameworkFiles: [],
|
|
107
|
+
userFiles: [],
|
|
108
|
+
hasAptVersion: false,
|
|
109
|
+
hasManifest: false,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const onDisk = walkRelative(installRoot)
|
|
114
|
+
const frameworkSet = new Set(LEGACY.framework_files)
|
|
115
|
+
const frameworkFiles = onDisk.filter((p) => frameworkSet.has(p))
|
|
116
|
+
const userFiles = onDisk.filter((p) => !frameworkSet.has(p) && p !== MANIFEST_FILENAME)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
present: true,
|
|
120
|
+
frameworkFiles,
|
|
121
|
+
userFiles,
|
|
122
|
+
hasAptVersion: onDisk.includes('.apt-version'),
|
|
123
|
+
hasManifest: onDisk.includes(MANIFEST_FILENAME),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Try to remove a directory if it is empty. Swallows ENOTEMPTY / ENOENT
|
|
129
|
+
* so the caller can blindly sweep candidate dirs in any order.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} dir
|
|
132
|
+
* @returns {boolean} true iff the dir was actually removed.
|
|
133
|
+
*/
|
|
134
|
+
function rmEmptyDir(dir) {
|
|
135
|
+
if (!existsSync(dir)) return false
|
|
136
|
+
try {
|
|
137
|
+
rmdirSync(dir)
|
|
138
|
+
return true
|
|
139
|
+
} catch {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Clean up the legacy `.agents/skills/` tree if it looks orphaned.
|
|
146
|
+
*
|
|
147
|
+
* Branches:
|
|
148
|
+
* 1. `present: false` OR `frameworkFiles.length === 0` → no-op,
|
|
149
|
+
* `{ ran: false, reason: 'no-legacy' }`.
|
|
150
|
+
* 2. `hasManifest: true` → managed install, no-op,
|
|
151
|
+
* `{ ran: false, reason: 'managed' }`.
|
|
152
|
+
* 3. Otherwise: per-file `unlinkSync` each framework-owned file;
|
|
153
|
+
* `rmdirSync` any now-empty `apt-*` subdirectory; sweep
|
|
154
|
+
* `.agents/skills/` only if it is empty; sweep `.agents/` only if
|
|
155
|
+
* its `skills/` got removed and `.agents/` is empty.
|
|
156
|
+
*
|
|
157
|
+
* Returns `{ ran: true, removed, retained, rmdirs }` on the cleanup
|
|
158
|
+
* branch:
|
|
159
|
+
* - `removed` — relative paths under `install_root` that were unlinked.
|
|
160
|
+
* - `retained` — relative paths under `install_root` that were NOT
|
|
161
|
+
* removed (user-added skills / files not in the legacy set).
|
|
162
|
+
* - `rmdirs` — absolute paths of directories that were removed
|
|
163
|
+
* (apt-* subdirs, the install_root itself, .agents/), in removal order.
|
|
164
|
+
*
|
|
165
|
+
* Atomicity: per-file unlink is non-atomic. A partial-failure scenario
|
|
166
|
+
* (process killed mid-loop) is tolerable — the next `init` run will
|
|
167
|
+
* re-detect the remaining framework files and finish the sweep.
|
|
168
|
+
*
|
|
169
|
+
* @param {{ targetDir: string, dryRun?: boolean }} args
|
|
170
|
+
* @returns {
|
|
171
|
+
* | { ran: false, reason: 'no-legacy' | 'managed' }
|
|
172
|
+
* | { ran: true, removed: string[], retained: string[], rmdirs: string[] }
|
|
173
|
+
* }
|
|
174
|
+
*/
|
|
175
|
+
export function runLegacyCleanup({ targetDir, dryRun = false }) {
|
|
176
|
+
const detection = detectLegacyCodexTree(targetDir)
|
|
177
|
+
|
|
178
|
+
if (!detection.present || detection.frameworkFiles.length === 0) {
|
|
179
|
+
return { ran: false, reason: 'no-legacy' }
|
|
180
|
+
}
|
|
181
|
+
if (detection.hasManifest) {
|
|
182
|
+
return { ran: false, reason: 'managed' }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const installRoot = join(targetDir, LEGACY.install_root)
|
|
186
|
+
const removed = []
|
|
187
|
+
const rmdirs = []
|
|
188
|
+
|
|
189
|
+
if (dryRun) {
|
|
190
|
+
// Dry-run: report what WOULD be removed without touching the filesystem.
|
|
191
|
+
// `retained` is the user-file set from the pre-walk (no sweep occurred).
|
|
192
|
+
// `rmdirs` is the set of apt-* subdirs that would become empty after removal.
|
|
193
|
+
const wouldRemove = detection.frameworkFiles
|
|
194
|
+
const retained = detection.userFiles
|
|
195
|
+
const candidateDirs = new Set()
|
|
196
|
+
for (const rel of wouldRemove) {
|
|
197
|
+
let parent = dirname(rel)
|
|
198
|
+
while (parent !== '.' && parent !== '') {
|
|
199
|
+
candidateDirs.add(parent)
|
|
200
|
+
parent = dirname(parent)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const wouldRmdirs = [...candidateDirs]
|
|
204
|
+
.sort((a, b) => b.length - a.length)
|
|
205
|
+
.map((rel) => join(installRoot, rel))
|
|
206
|
+
return { ran: true, dryRun: true, removed: wouldRemove, retained, rmdirs: wouldRmdirs }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const rel of detection.frameworkFiles) {
|
|
210
|
+
const abs = join(installRoot, rel)
|
|
211
|
+
try {
|
|
212
|
+
unlinkSync(abs)
|
|
213
|
+
removed.push(rel)
|
|
214
|
+
} catch {
|
|
215
|
+
/* best-effort: leave a partial sweep; next run finishes it */
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Sweep any now-empty `apt-*/` subdirs the unlinks left behind.
|
|
220
|
+
// Walk parent dirs of every removed file in reverse-depth order so
|
|
221
|
+
// nested `apt-pr-review/prompts/` empties before `apt-pr-review/`.
|
|
222
|
+
const candidateDirs = new Set()
|
|
223
|
+
for (const rel of removed) {
|
|
224
|
+
let parent = dirname(rel)
|
|
225
|
+
while (parent !== '.' && parent !== '') {
|
|
226
|
+
candidateDirs.add(parent)
|
|
227
|
+
parent = dirname(parent)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const sortedDirs = [...candidateDirs].sort((a, b) => b.length - a.length)
|
|
231
|
+
for (const rel of sortedDirs) {
|
|
232
|
+
const abs = join(installRoot, rel)
|
|
233
|
+
if (rmEmptyDir(abs)) rmdirs.push(abs)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Sweep `.agents/skills/` only if fully empty after the per-file
|
|
237
|
+
// pass — preserves any user-added skill dirs.
|
|
238
|
+
if (rmEmptyDir(installRoot)) {
|
|
239
|
+
rmdirs.push(installRoot)
|
|
240
|
+
// Sweep `.agents/` only if it became empty too (i.e. no other
|
|
241
|
+
// `.agents/<sibling>` content the user keeps there).
|
|
242
|
+
const agentsRoot = dirname(installRoot)
|
|
243
|
+
if (rmEmptyDir(agentsRoot)) rmdirs.push(agentsRoot)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Use the pre-walk's userFiles as `retained` — the post-walk only existed
|
|
247
|
+
// to defensively re-scan, but pre-walk already captured user files accurately.
|
|
248
|
+
// (PRF-002: avoid redundant detectLegacyCodexTree call.)
|
|
249
|
+
const retained = detection.userFiles
|
|
250
|
+
|
|
251
|
+
return { ran: true, removed, retained, rmdirs }
|
|
252
|
+
}
|