@aperant/framework 0.6.7 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/dist/cli/commands/ci-watch.d.mts.map +1 -1
- package/dist/cli/commands/ci-watch.mjs +39 -1
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/health-check.mjs +1 -1
- package/dist/cli/commands/health-check.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts +10 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +94 -4
- package/dist/cli/commands/init.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 +48 -0
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +1 -0
- 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-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/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/package.json +1 -1
- package/skills/apt-close-task/SKILL.md +5 -0
- 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-watch-ci/SKILL.md +4 -1
- package/src/cli/commands/ci-watch.mjs +39 -1
- package/src/cli/commands/health-check.mjs +1 -1
- package/src/cli/commands/init.mjs +102 -2
- package/src/cli/commands/route.mjs +38 -2
- package/src/cli/commands/task.mjs +49 -0
- package/src/cli/help.mjs +1 -0
- package/src/cli/host/detect.mjs +135 -0
- package/src/cli/install/legacy-paths.mjs +69 -0
- package/src/cli/install/runtime-migrate.mjs +252 -0
- package/src/cli/route/drift-detect.mjs +107 -0
- 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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preflight check: is the installed kernel new enough to support the
|
|
3
|
+
* named feature?
|
|
4
|
+
*
|
|
5
|
+
* - Returns `null` when the install is at-or-above `introducedIn` (pass).
|
|
6
|
+
* - Returns `null` when `installedVersion` is malformed (fail-open — a
|
|
7
|
+
* corrupted `package.json` shouldn't lock users out of every cmd; the
|
|
8
|
+
* route-level drift check still catches the same install via the
|
|
9
|
+
* version mismatch path).
|
|
10
|
+
* - Returns a `version_drift` error envelope when the install predates
|
|
11
|
+
* `introducedIn`. Callers `exitWith(envelope, 1)`.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} args
|
|
14
|
+
* @param {string} args.feature Short name of the feature being gated.
|
|
15
|
+
* @param {string} args.introducedIn Minimum framework version that ships the feature.
|
|
16
|
+
* @param {string} args.installedVersion The installed kernel's version.
|
|
17
|
+
* @param {string} args.missingFlag The CLI flag that requires this version (e.g. '--payload-file').
|
|
18
|
+
* @returns {null | {
|
|
19
|
+
* status: 'error',
|
|
20
|
+
* command: 'version-preflight',
|
|
21
|
+
* error_code: 'version_drift',
|
|
22
|
+
* feature: string,
|
|
23
|
+
* introduced_in: string,
|
|
24
|
+
* installed_version: string,
|
|
25
|
+
* missing_flag: string,
|
|
26
|
+
* reason: string,
|
|
27
|
+
* }}
|
|
28
|
+
*/
|
|
29
|
+
export function requireMinVersion({ feature, introducedIn, installedVersion, missingFlag }: {
|
|
30
|
+
feature: string;
|
|
31
|
+
introducedIn: string;
|
|
32
|
+
installedVersion: string;
|
|
33
|
+
missingFlag: string;
|
|
34
|
+
}): null | {
|
|
35
|
+
status: "error";
|
|
36
|
+
command: "version-preflight";
|
|
37
|
+
error_code: "version_drift";
|
|
38
|
+
feature: string;
|
|
39
|
+
introduced_in: string;
|
|
40
|
+
installed_version: string;
|
|
41
|
+
missing_flag: string;
|
|
42
|
+
reason: string;
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=version-preflight.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version-preflight.d.mts","sourceRoot":"","sources":["../../../src/cli/util/version-preflight.mjs"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,4FAfG;IAAqB,OAAO,EAApB,MAAM;IACO,YAAY,EAAzB,MAAM;IACO,gBAAgB,EAA7B,MAAM;IACO,WAAW,EAAxB,MAAM;CACd,GAAU,IAAI,GAAG;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB,CAmBH"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* util/version-preflight.mjs — FRAMEWORK-BUG-017 (rung 2 of 3).
|
|
3
|
+
*
|
|
4
|
+
* Pure helper for cmds that depend on a flag / behavior introduced in a
|
|
5
|
+
* specific framework version. When the installed kernel predates that
|
|
6
|
+
* version, returning a structured `version_drift` envelope is strictly
|
|
7
|
+
* better than the silent infra-backoff loop that bit us in
|
|
8
|
+
* FRAMEWORK-BUG-010.
|
|
9
|
+
*
|
|
10
|
+
* Scope (Codex review edit 1): this is a **future-guard** for the next
|
|
11
|
+
* framework fix that introduces a new flag. A stale 0.6.6 install
|
|
12
|
+
* obviously cannot execute 0.6.8 preflight code (the helper doesn't
|
|
13
|
+
* exist there). The actual catch-point for already-stale installs is
|
|
14
|
+
* the route-side `detectSourceWorkspaceDrift`. This helper makes the
|
|
15
|
+
* NEXT bug bundle cheaper to ship safely.
|
|
16
|
+
*/
|
|
17
|
+
import { compareVersions, SEMVER_RE } from './semver.mjs';
|
|
18
|
+
/**
|
|
19
|
+
* Preflight check: is the installed kernel new enough to support the
|
|
20
|
+
* named feature?
|
|
21
|
+
*
|
|
22
|
+
* - Returns `null` when the install is at-or-above `introducedIn` (pass).
|
|
23
|
+
* - Returns `null` when `installedVersion` is malformed (fail-open — a
|
|
24
|
+
* corrupted `package.json` shouldn't lock users out of every cmd; the
|
|
25
|
+
* route-level drift check still catches the same install via the
|
|
26
|
+
* version mismatch path).
|
|
27
|
+
* - Returns a `version_drift` error envelope when the install predates
|
|
28
|
+
* `introducedIn`. Callers `exitWith(envelope, 1)`.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} args
|
|
31
|
+
* @param {string} args.feature Short name of the feature being gated.
|
|
32
|
+
* @param {string} args.introducedIn Minimum framework version that ships the feature.
|
|
33
|
+
* @param {string} args.installedVersion The installed kernel's version.
|
|
34
|
+
* @param {string} args.missingFlag The CLI flag that requires this version (e.g. '--payload-file').
|
|
35
|
+
* @returns {null | {
|
|
36
|
+
* status: 'error',
|
|
37
|
+
* command: 'version-preflight',
|
|
38
|
+
* error_code: 'version_drift',
|
|
39
|
+
* feature: string,
|
|
40
|
+
* introduced_in: string,
|
|
41
|
+
* installed_version: string,
|
|
42
|
+
* missing_flag: string,
|
|
43
|
+
* reason: string,
|
|
44
|
+
* }}
|
|
45
|
+
*/
|
|
46
|
+
export function requireMinVersion({ feature, introducedIn, installedVersion, missingFlag }) {
|
|
47
|
+
if (!installedVersion || typeof installedVersion !== 'string')
|
|
48
|
+
return null;
|
|
49
|
+
if (!SEMVER_RE.test(installedVersion))
|
|
50
|
+
return null;
|
|
51
|
+
if (!SEMVER_RE.test(introducedIn))
|
|
52
|
+
return null;
|
|
53
|
+
if (compareVersions(installedVersion, introducedIn) >= 0)
|
|
54
|
+
return null;
|
|
55
|
+
return {
|
|
56
|
+
status: 'error',
|
|
57
|
+
command: 'version-preflight',
|
|
58
|
+
error_code: 'version_drift',
|
|
59
|
+
feature,
|
|
60
|
+
introduced_in: introducedIn,
|
|
61
|
+
installed_version: installedVersion,
|
|
62
|
+
missing_flag: missingFlag,
|
|
63
|
+
reason: `cmd needs feature ${feature} introduced in version ${introducedIn}; installed version is ${installedVersion} — run /apt:update`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=version-preflight.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version-preflight.mjs","sourceRoot":"","sources":["../../../src/cli/util/version-preflight.mjs"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE;IACzF,IAAI,CAAC,gBAAgB,IAAI,OAAO,gBAAgB,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAC1E,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,gBAAgB,CAAC;QAAE,OAAO,IAAI,CAAA;IAClD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAA;IAE9C,IAAI,eAAe,CAAC,gBAAgB,EAAE,YAAY,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAErE,OAAO;QACN,MAAM,EAAE,OAAO;QACf,OAAO,EAAE,mBAAmB;QAC5B,UAAU,EAAE,eAAe;QAC3B,OAAO;QACP,aAAa,EAAE,YAAY;QAC3B,iBAAiB,EAAE,gBAAgB;QACnC,YAAY,EAAE,WAAW;QACzB,MAAM,EAAE,qBAAqB,OAAO,0BAA0B,YAAY,0BAA0B,gBAAgB,oBAAoB;KACxI,CAAA;AACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aperant/framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "AI coding framework — composable skills for planning, executing, verifying, and reviewing code with any LLM provider. Works as Claude Code commands, Codex tasks, or programmatically.",
|
|
5
5
|
"author": "Mikalsen AI <hello@mikalsen.ai>",
|
|
6
6
|
"type": "module",
|
|
@@ -123,6 +123,8 @@ close-merged emits an envelope with:
|
|
|
123
123
|
|
|
124
124
|
Report the summary verbatim. If anything landed in `skipped[]` with a recoverable reason (`offline`, `rate_limited`), suggest the user re-run once the network clears.
|
|
125
125
|
|
|
126
|
+
**Local cleanup recap (best-effort).** On confirmed merge, `task close-merged` runs `computeWorktreeCleanup` against the primary repo (`packages/framework/src/cli/task/worktree-cleanup.mjs:140-230`): `git fetch origin <base>` (failure → recorded as `fetch_failed` warning, cleanup proceeds), `git checkout <base>` (failure → returns `action: 'checkout_failed'` and preserves BOTH the worktree and the task branch — nothing is removed), `git merge --ff-only origin/<base>` (failure → `ff_only_failed` warning, GC still runs), `git worktree remove <wtPath>`, and `git branch -d <taskBranch>` (failure on squash/rebase merges → `branch_delete_failed` warning, `branch_deleted: false` in the envelope; user can `git branch -D` manually). This is why `/apt:watch-ci` auto-merge MUST NOT pass `gh pr merge --delete-branch` — that flag would try to do the same work from inside the worktree where `git switch <base>` cannot succeed (see FRAMEWORK-BUG-019).
|
|
127
|
+
|
|
126
128
|
## 4. Narrate-only mode
|
|
127
129
|
|
|
128
130
|
When invoked with `--narrate-only`, skip §1 (no `gh pr view` polling) and §2 (no destructive close path). The deterministic close already ran via the passive post-merge sweep — your job is to drain the `state.pending_narration[]` ledger the sweep parked.
|
|
@@ -133,10 +135,13 @@ Flags:
|
|
|
133
135
|
|
|
134
136
|
Per ledger entry `{ task_id, scope, phase_id, closed_at, pr_number }`:
|
|
135
137
|
|
|
138
|
+
0. **Enumerate the ledger first.** Run `apt-tools task close-merged . --narrate-only [--task <id>]`. The envelope's `closed[]` is the read-only list of pending narration rows (each carries `narration_pending: true`); state.json is not mutated. When `closed[]` is empty, exit early with `{status: 'ok', narrated: [], skipped: [], remaining: 0}` — there is nothing to drain.
|
|
136
139
|
1. Run `apt-tools features-audit . --apply-stubs --task <task_id>` so the feature registry catches whatever the closed task introduced. If the subprocess exits non-zero, skip the entry with `reason: 'features-audit-failed'` — never block narration on registry update.
|
|
137
140
|
2. When `phase_id` is non-null, spawn `apt-team-docs-narrator` (background, fire-and-forget) using the same shape as §2.5: `subagent_type: apt-team-docs-narrator`, `run_in_background: true`, `description: Narrate shipped phase {phase_id}`, `prompt: Run in phase-ship mode with --phase {phase_id} --scope {scope}.`. When `phase_id` is null, skip the spawn with `reason: 'no-phase-id'` (quick tasks have no phase to narrate).
|
|
138
141
|
3. On successful spawn (or successful no-phase skip), drain the row via `apt-tools task narration-drain . --task <task_id>` which acquires `withFileLock(state.json)` and removes the matching `pending_narration[]` entry. On spawn failure, leave the row in place with `reason: 'narrator-spawn-failed'` so the user can re-invoke.
|
|
139
142
|
|
|
143
|
+
**At-least-once retry semantics (FRAMEWORK-BUG-020).** Step 0 is read-only — it does NOT remove rows. Steps 1–3 are the existing per-row drain contract: a row stays in `pending_narration[]` until step 3 calls `narration-drain` successfully. If the orchestrator crashes between step 2 (narrator spawn) and step 3 (drain), the row remains parked and a retry will spawn narration again. Re-spawning is safe because the narrator is **anchor-replace idempotent** by design (see `agents/apt-team-docs-narrator.md` §6 / §2 and the "Idempotent operations only" rule in §notes) — it uses HTML-comment anchors to bound the replaced region in ROADMAP.md, so a second run replaces the same span rather than appending.
|
|
144
|
+
|
|
140
145
|
Emit a summary envelope:
|
|
141
146
|
|
|
142
147
|
```json
|
package/skills/apt-ship/SKILL.md
CHANGED
|
@@ -412,19 +412,30 @@ ci_watch_armed=$(echo "$ship_update" | jq -r '.ci_watch_armed // false')
|
|
|
412
412
|
ci_watch_skip_reason=$(echo "$ship_update" | jq -r '.ci_watch_skip_reason // empty')
|
|
413
413
|
ci_watch_host=$(echo "$ship_update" | jq -r '.ci_watch.host // "unknown"')
|
|
414
414
|
ci_watch_deadline=$(echo "$ship_update" | jq -r '.ci_watch.deadline // empty')
|
|
415
|
+
ci_watch_scheduler_handle=$(echo "$ship_update" | jq -r '.scheduler_handle // empty')
|
|
415
416
|
host_cron=$(node packages/framework/bin/apt-tools.mjs host-detect | jq -r '.host.capabilities.cron')
|
|
416
417
|
```
|
|
417
418
|
|
|
418
419
|
**Autonomy interaction** (apply BEFORE branching, same rule as 7.1): when `autonomy === 3` (YOLO), degrade `ask` → `auto`.
|
|
419
420
|
|
|
420
|
-
**Branch on the task-update envelope
|
|
421
|
+
**Branch on the task-update envelope** (FRAMEWORK-BUG-018 — Track 1: distinguish state-recording from wake-registration):
|
|
421
422
|
|
|
422
|
-
- **`ci_watch_armed=true` AND `host_cron=true
|
|
423
|
+
- **`ci_watch_armed=true` AND `host_cron=true` AND `ci_watch_scheduler_handle` non-empty** — emit:
|
|
423
424
|
|
|
424
425
|
```
|
|
425
426
|
[APT:ship] CI watcher armed (host cron: {ci_watch_host}). Deadline: {ci_watch_deadline}. Stop: /apt:watch-ci --stop --pr {N}.
|
|
426
427
|
```
|
|
427
428
|
|
|
429
|
+
This is the only honest "armed" case — a scheduler handle proves the wake is registered.
|
|
430
|
+
|
|
431
|
+
- **`ci_watch_armed=true` AND `host_cron=true` AND `ci_watch_scheduler_handle` empty** — emit:
|
|
432
|
+
|
|
433
|
+
```
|
|
434
|
+
[APT:ship] CI watcher state created (host: {ci_watch_host}). Register a wake-up to drive ticks: CronCreate every ~3min running '/apt:watch-ci --tick --pr {N}'. Deadline: {ci_watch_deadline}. Stop: /apt:watch-ci --stop --pr {N}.
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
State is recorded but no scheduler is registered — the orchestrator MUST register a wake or ticks will never fire. Track 2 (auto-wake on arm) will populate `scheduler_handle` and route into the first branch instead.
|
|
438
|
+
|
|
428
439
|
- **`ci_watch_armed=true` AND `host_cron=false`** — emit:
|
|
429
440
|
|
|
430
441
|
```
|
|
@@ -442,10 +453,11 @@ host_cron=$(node packages/framework/bin/apt-tools.mjs host-detect | jq -r '.host
|
|
|
442
453
|
On `Y` (or empty enter), call:
|
|
443
454
|
|
|
444
455
|
```bash
|
|
445
|
-
node packages/framework/bin/apt-tools.mjs ci-watch start . --pr {pr_number} --task-id {task-id} --host {cli}
|
|
456
|
+
arm_envelope=$(node packages/framework/bin/apt-tools.mjs ci-watch start . --pr {pr_number} --task-id {task-id} --host {cli})
|
|
457
|
+
ci_watch_scheduler_handle=$(echo "$arm_envelope" | jq -r '.scheduler_handle // empty')
|
|
446
458
|
```
|
|
447
459
|
|
|
448
|
-
Then
|
|
460
|
+
Then route into the same four-way logic above — branch 1 (armed with handle), branch 2 (state-created, needs wake), or branch 3 (host has no cron). On `N` or any other response, proceed silently to Section 8.
|
|
449
461
|
|
|
450
462
|
- **`ci_watch_skip_reason=disabled`** — silent. Proceed directly to Section 8.
|
|
451
463
|
|
package/skills/apt-spar/SKILL.md
CHANGED
|
@@ -129,14 +129,26 @@ If `--with` resolves to the same CLI as the host (e.g. `--with claude` when `hos
|
|
|
129
129
|
|
|
130
130
|
Do NOT proceed; self-sparring is sycophancy by construction (the host cannot objectively challenge itself). This check runs after `host-detect` and before any partner availability check.
|
|
131
131
|
|
|
132
|
-
**Partner
|
|
132
|
+
**Partner transport ladder (FRAMEWORK-BUG-024).** Read `host.partner_transports.<partner>` from the host-detect envelope (`jq '.host.partner_transports.codex'`, etc.). Each partner has a 3-rung ladder evaluated in preference order — **MCP-first** for cleaner tool-call integration, with automatic fallthrough to plugin shim then raw CLI:
|
|
133
|
+
|
|
134
|
+
1. **Rung 1 — MCP tool** (`mcp__<partner>-mcp__*`, e.g. `mcp__codex-mcp__codex`). Detected if the host exposes its deferred-tool manifest via `CLAUDE_MCP_DEFERRED_TOOLS` and the tool is registered. When `detected: "runtime-host-only"` the framework process cannot probe the host's tool registry from outside — treat as candidate and attempt the tool call; if the tool is genuinely unavailable in the host's visible registry, fall through to rung 2.
|
|
135
|
+
2. **Rung 2 — Plugin shim** (`${CLAUDE_PLUGIN_ROOT}/scripts/<partner>-companion.mjs`). Codex only (claude / gemini have no plugin shim).
|
|
136
|
+
3. **Rung 3 — Raw CLI on `$PATH`** (`codex exec`, `claude -p`, `gemini -m`).
|
|
137
|
+
|
|
138
|
+
Walk the ladder in order; pick the first rung with `detected: true` (or `"runtime-host-only"` for MCP). **MCP rung has a hard timeout** — default 60000ms, configurable via `apt-spar.mcp_timeout_ms` in `.aperant/config.json`. Empirical: the Codex MCP `claude_code` tool has been observed to hang past 120s. On MCP timeout, **automatically fall through to rung 2 or rung 3** — do NOT fail the whole spar. Treat MCP timeout as "rung unavailable for this invocation."
|
|
139
|
+
|
|
140
|
+
If all three rungs are unavailable (every rung `detected: false`, or MCP `runtime-host-only` timed out AND rungs 2+3 are `detected: false`), print:
|
|
133
141
|
|
|
134
142
|
```
|
|
135
|
-
[apt:spar] partner `<id>` unavailable.
|
|
136
|
-
-
|
|
137
|
-
-
|
|
143
|
+
[apt:spar] partner `<id>` unavailable. Tried:
|
|
144
|
+
- MCP tool `mcp__<partner>-mcp__*`: <not loaded in this host | runtime-host-only + timed out at <ms>ms>
|
|
145
|
+
- Plugin shim `${CLAUDE_PLUGIN_ROOT}/scripts/<partner>-companion.mjs`: <missing reason>
|
|
146
|
+
- Raw CLI `<bin>` on $PATH: not found
|
|
147
|
+
Install any one of the three or re-run with `--with <other-id>`.
|
|
138
148
|
```
|
|
139
149
|
|
|
150
|
+
The failure message MUST name all three transports tried (`mcp`, `plugin shim`, `raw CLI`) so the user can pick the cheapest fix.
|
|
151
|
+
|
|
140
152
|
Do NOT silently degrade to the host (sparring with yourself is sycophancy by construction).
|
|
141
153
|
|
|
142
154
|
## 4. Frame the Round
|
|
@@ -159,13 +171,26 @@ The frame is one shot per round — no multi-turn conversation with the partner
|
|
|
159
171
|
|
|
160
172
|
**Wrap every partner Bash call with an explicit timeout** (default 90000ms, override via `--timeout-ms`). The empirical reason: the Codex MCP `claude_code` tool has been observed to hang past 120s while the `claude -p` CLI returns in seconds. A hung partner CLI must NOT stall the spar loop.
|
|
161
173
|
|
|
162
|
-
The table below shows the canonical command shape; the actual invocation MUST use the heredoc pattern in the next subsection — never raw double-quoted interpolation.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
174
|
+
The table below shows the canonical command shape per (partner × rung); the actual invocation for shell-based rungs (plugin shim, raw CLI) MUST use the heredoc pattern in the next subsection — never raw double-quoted interpolation. The MCP rung is a structured tool call — no shell quoting concern applies.
|
|
175
|
+
|
|
176
|
+
<!--
|
|
177
|
+
Future contributor: the `codex exec` flag below is taken from the precedent
|
|
178
|
+
at `packages/framework/skills/apt-pr-review/SKILL.md:1084`. Codex CLI flag
|
|
179
|
+
surface evolves; verify with `codex --help` at execution time and update
|
|
180
|
+
this table if the non-interactive invocation shape changes.
|
|
181
|
+
-->
|
|
182
|
+
|
|
183
|
+
| Partner | Rung | Command | Notes |
|
|
184
|
+
|---|---|---|---|
|
|
185
|
+
| codex | mcp | tool call: `mcp__codex-mcp__codex({prompt})` | Preferred. 60s timeout (configurable `apt-spar.mcp_timeout_ms`); on timeout fall through to plugin then CLI. |
|
|
186
|
+
| codex | plugin | `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | **NO `--write`. NO `--effort` override.** Per SPAR-04 — spar is conversation, not patching. |
|
|
187
|
+
| codex | cli | `codex exec "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | Precedent: `apt-pr-review/SKILL.md:1084`. Verify with `codex --help`. |
|
|
188
|
+
| claude | mcp | tool call: `mcp__claude-code-mcp__claude_code({prompt})` | Preferred. Same 60s timeout + fallthrough rule. |
|
|
189
|
+
| claude | plugin | n/a | No plugin shim exists for Claude. Skip this rung. |
|
|
190
|
+
| claude | cli | `claude -p "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)" --output-format text` | Symmetric. Claude has no `--write` equivalent — asymmetric flags are a smell. |
|
|
191
|
+
| gemini | mcp | tool call: `mcp__gemini-mcp__*({prompt})` | Preferred. Same 60s timeout + fallthrough rule. Any `mcp__gemini-mcp__*` tool satisfies the rung. |
|
|
192
|
+
| gemini | plugin | n/a | No plugin shim exists for Gemini. Skip this rung. |
|
|
193
|
+
| gemini | cli | `gemini -m <model-from-router.llm.providers.gemini-cli> "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | Read the exact model name from `.aperant/config.json` at runtime — do not hardcode. |
|
|
169
194
|
|
|
170
195
|
Set the Bash tool's `timeout` parameter to `--timeout-ms` (default 90000).
|
|
171
196
|
|
|
@@ -15,7 +15,7 @@ execution_modes:
|
|
|
15
15
|
- auto
|
|
16
16
|
- step
|
|
17
17
|
allowed-tools: "Bash, Read, Write"
|
|
18
|
-
argument-hint: "apt:update"
|
|
18
|
+
argument-hint: "apt:update [--from-source]"
|
|
19
19
|
gates: []
|
|
20
20
|
---
|
|
21
21
|
|
|
@@ -90,6 +90,30 @@ npm install --prefix .aperant/deps @aperant/framework@<latest_version> --no-save
|
|
|
90
90
|
|
|
91
91
|
On `npm 404` (`E404` in stderr) treat it as `not_published` and skip the kernel bump (proceed to step 3). On any other npm failure, surface the npm stderr verbatim and STOP — do NOT proceed to init with a half-installed kernel.
|
|
92
92
|
|
|
93
|
+
## 1.6. Self-host case — bump from local workspace (FRAMEWORK-BUG-017)
|
|
94
|
+
|
|
95
|
+
When the user invokes `/apt:update --from-source` OR the route envelope's `update_check.reason` mentions `workspace source ahead of installed kernel`, the source of truth is the local `packages/framework/` workspace — NOT npm. This is the Aperant-contributor self-host case: a same-PR framework fix bumps the workspace `packages/framework/package.json:version` but the kernel under `.aperant/deps/node_modules/@aperant/framework/` still reflects the previous version. Step 1.5's `npm view` would fetch the published version (still the OLD one), so we must skip it.
|
|
96
|
+
|
|
97
|
+
The reliable bump mechanism mirrors what `runKernelInstall` does today — `pnpm pack` produces a versioned tarball, then `npm install --prefix` consumes it. Symlinking is fragile across re-installs and `pnpm install --filter` doesn't relocate the published artifact into `.aperant/deps`.
|
|
98
|
+
|
|
99
|
+
Walk up from the project root looking for a sibling `packages/framework/package.json`. When found AND its `name === "@aperant/framework"`:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# 1. Pack the workspace source into a versioned tarball.
|
|
103
|
+
cd <workspace-root>
|
|
104
|
+
pnpm --filter @aperant/framework pack --pack-destination /tmp
|
|
105
|
+
|
|
106
|
+
# 2. Install the tarball into the project's kernel root.
|
|
107
|
+
cd <project-root>
|
|
108
|
+
npm install --prefix .aperant/deps /tmp/aperant-framework-<version>.tgz --no-save --no-package-lock --no-audit --no-fund
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Replace `<version>` with the workspace `package.json:version` you just read. The tarball name follows npm's `pack` convention (`@aperant/framework@X.Y.Z` → `aperant-framework-X.Y.Z.tgz`).
|
|
112
|
+
|
|
113
|
+
If `--from-source` was passed but no `packages/framework/package.json` is found anywhere above the project root, surface a one-line error (`⚠ --from-source: no workspace package.json found — falling back to npm`) and proceed with the standard step 1.5 npm path.
|
|
114
|
+
|
|
115
|
+
Then proceed to step 3 to re-install the runtime files at the new kernel version. Skip step 4 (cache clear) — the cache is keyed on `latest_version` from npm, and a `--from-source` bump never wrote to it, so there's nothing to invalidate.
|
|
116
|
+
|
|
93
117
|
## 2. Classify the update scenario
|
|
94
118
|
|
|
95
119
|
Pick ONE of three scenarios based on what you parsed in step 1:
|
|
@@ -206,5 +230,6 @@ Runtime files were still refreshed; `/apt` will still work from PATH if `apt-too
|
|
|
206
230
|
- **Worker hasn't run yet**: the cache file is missing. That's fine — run check-version directly (step 1 is robust to a missing cache). Proceed normally.
|
|
207
231
|
- **Re-install fails (non-zero exit from init)**: surface the JSON `error` field verbatim in a single line, then stop. Do NOT delete the cache file — the chip should remain visible so the user sees the failure persists.
|
|
208
232
|
- **Kernel install fails but runtime install succeeds**: see Scenario D above. Do not treat this as a total failure; the user's skills still work via PATH fallback.
|
|
233
|
+
- **CLAUDE.md / AGENTS.md foreign rows (exit 2, `instruction-file.foreign-rows`)** — 0.7.0+ — init now refuses to silently clobber custom rows in the `<!-- APT:framework-* -->` block. If `init` exits 2 with `code: instruction-file.foreign-rows`, the `foreignRowIds[]` field lists the rows the generator did NOT produce. Two paths: (a) remove the foreign rows from CLAUDE.md / AGENTS.md by hand and re-run init, or (b) re-run with `--force-overwrite-catalog` to regenerate anyway. Unattended upgrade flows should detect this code and surface it to the operator — silent bypass is a regression.
|
|
209
234
|
|
|
210
235
|
</edge_cases>
|
|
@@ -87,7 +87,10 @@ For the `--tick` mode, execute exactly the following sequence — once — insid
|
|
|
87
87
|
|
|
88
88
|
When `action ∈ {green, merged, closed, attempts-cap, wall-cap, identical-log}`:
|
|
89
89
|
|
|
90
|
-
- **green** — print "CI green for PR {N}." If `ci_watch.auto_merge_on_green` is `true` in `.aperant/config.json`, run `gh pr merge {N} --auto --squash` then suggest `/apt:close-task`.
|
|
90
|
+
- **green** — print "CI green for PR {N}." If `ci_watch.auto_merge_on_green` is `true` in `.aperant/config.json`, run `gh pr merge {N} --auto --squash` then suggest `/apt:close-task`.
|
|
91
|
+
- **DO NOT pass `--delete-branch`** to `gh pr merge` from this auto-merge path — ever, unconditionally. The watcher's cwd is the task worktree, where `gh`'s `--delete-branch` post-merge cleanup tries to `git switch <base>` and fails because `<base>` is checked out in the primary worktree. The remote merge has already succeeded by then; local branch + worktree cleanup is `/apt:close-task`'s job via `apt-tools task close-merged` → `computeWorktreeCleanup` (see FRAMEWORK-BUG-019).
|
|
92
|
+
|
|
93
|
+
Else print the exact D-08 text:
|
|
91
94
|
|
|
92
95
|
```
|
|
93
96
|
✅ CI green — merge in browser or run /apt:close-task post-merge
|
|
@@ -26,12 +26,33 @@
|
|
|
26
26
|
|
|
27
27
|
import { randomBytes } from 'node:crypto'
|
|
28
28
|
import { existsSync, readFileSync } from 'node:fs'
|
|
29
|
-
import { resolve } from 'node:path'
|
|
29
|
+
import { dirname, resolve } from 'node:path'
|
|
30
|
+
import { fileURLToPath } from 'node:url'
|
|
30
31
|
import { acquireLock, releaseLock } from '../ci-watch/lock.mjs'
|
|
31
32
|
import { deleteState, listWatches, readState, writeState } from '../ci-watch/state.mjs'
|
|
32
33
|
import { classify, isFailing } from '../ci-watch/stop-matrix.mjs'
|
|
33
34
|
import { parseFlags } from '../util/args.mjs'
|
|
34
35
|
import { err, exitWith, ok } from '../util/result.mjs'
|
|
36
|
+
import { requireMinVersion } from '../util/version-preflight.mjs'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Installed kernel version — read once at module scope, same pattern as
|
|
40
|
+
* commands/check-version.mjs:108-109. Used by the version-preflight in
|
|
41
|
+
* `doTick` to fail loud (instead of infra-backoff-loop) when a future
|
|
42
|
+
* framework fix introduces a new tick flag and the user is on a stale
|
|
43
|
+
* install. See FRAMEWORK-BUG-017 rung 2 + the helper docstring for the
|
|
44
|
+
* "future-guard" framing.
|
|
45
|
+
*/
|
|
46
|
+
const INSTALLED_KERNEL_VERSION = (() => {
|
|
47
|
+
try {
|
|
48
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
49
|
+
const pkgPath = resolve(here, '..', '..', '..', 'package.json')
|
|
50
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
51
|
+
return typeof pkg?.version === 'string' ? pkg.version : null
|
|
52
|
+
} catch {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
})()
|
|
35
56
|
|
|
36
57
|
const DEFAULT_TICK_INTERVAL_MS = 300_000 // 5 minutes
|
|
37
58
|
const MAX_TICK_INTERVAL_MS = 30 * 60 * 1000 // 30 min
|
|
@@ -119,12 +140,14 @@ function doStart(root, flags) {
|
|
|
119
140
|
command: 'ci-watch-start',
|
|
120
141
|
pr: Number(pr),
|
|
121
142
|
reason: 'already-armed',
|
|
143
|
+
scheduler_handle: existingState.scheduler_handle ?? null,
|
|
122
144
|
existing: {
|
|
123
145
|
task_id: existingState.task_id,
|
|
124
146
|
host: existingState.host,
|
|
125
147
|
attempts: existingState.attempts,
|
|
126
148
|
started_at: existingState.started_at,
|
|
127
149
|
deadline: existingState.deadline,
|
|
150
|
+
scheduler_handle: existingState.scheduler_handle ?? null,
|
|
128
151
|
},
|
|
129
152
|
},
|
|
130
153
|
2,
|
|
@@ -261,6 +284,21 @@ function summarize(s) {
|
|
|
261
284
|
// ---------------------------------------------------------------------------
|
|
262
285
|
|
|
263
286
|
function doTick(root, flags) {
|
|
287
|
+
// FRAMEWORK-BUG-017 rung 2 (future-guard): emit a structured
|
|
288
|
+
// version_drift envelope when a future framework fix requires a
|
|
289
|
+
// new tick flag and the installed kernel predates it. The current
|
|
290
|
+
// pin to 0.6.7 (the version that introduced --payload-file) is a
|
|
291
|
+
// no-op on every 0.6.7+ install — but the scaffolding is in place
|
|
292
|
+
// for the NEXT new-flag fix to bump `introducedIn` without
|
|
293
|
+
// re-discovering the silent-infra-backoff failure mode.
|
|
294
|
+
const versionGate = requireMinVersion({
|
|
295
|
+
feature: 'payload-file',
|
|
296
|
+
introducedIn: '0.6.7',
|
|
297
|
+
installedVersion: INSTALLED_KERNEL_VERSION,
|
|
298
|
+
missingFlag: '--payload-file',
|
|
299
|
+
})
|
|
300
|
+
if (versionGate) return exitWith(versionGate, 1)
|
|
301
|
+
|
|
264
302
|
const prCheck = validatePr(flags.get('pr'))
|
|
265
303
|
if (typeof prCheck !== 'string') return prCheck
|
|
266
304
|
const pr = prCheck
|
|
@@ -140,7 +140,7 @@ export function cmdHealthCheck(projectDir) {
|
|
|
140
140
|
issues.push({
|
|
141
141
|
type: 'missing-skills',
|
|
142
142
|
severity: 'warning',
|
|
143
|
-
detail: 'No
|
|
143
|
+
detail: 'No apt install manifests or version stamps found for any supported runtime',
|
|
144
144
|
install_surfaces: installs,
|
|
145
145
|
})
|
|
146
146
|
}
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
writeLastInstallSummary,
|
|
42
42
|
} from '../install/pipeline.mjs'
|
|
43
43
|
import { RUNTIMES, suggestRuntimes } from '../install/runtime-detect.mjs'
|
|
44
|
+
import { runLegacyCleanup } from '../install/runtime-migrate.mjs'
|
|
44
45
|
import {
|
|
45
46
|
CLASS_A_SETTINGS,
|
|
46
47
|
CLASS_B_SETTINGS,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
parseClassFlags,
|
|
49
50
|
} from '../install/setting-class.mjs'
|
|
50
51
|
import { detectAgentRuntime } from '../util/agent-runtime.mjs'
|
|
52
|
+
import { buildAptSection } from '../util/aperant-section.mjs'
|
|
51
53
|
import { atomicWriteJson } from '../util/atomic-write.mjs'
|
|
52
54
|
import { aperantBanner } from '../util/banner.mjs'
|
|
53
55
|
import { copyDirRecursive, injectInstructionFile } from '../util/copy.mjs'
|
|
@@ -88,8 +90,15 @@ const TRANSFORM_LOADERS = new Map(
|
|
|
88
90
|
* additive: callers without the flag still get the no-op short-circuit
|
|
89
91
|
* (preserves `init-upgrade-drift.test.ts` case (a)).
|
|
90
92
|
*
|
|
93
|
+
* 0.7.0 — `--force-overwrite-catalog` (FRAMEWORK-BUG-023) bypasses the
|
|
94
|
+
* foreign-row defensive check in `injectInstructionFile`. Power-user
|
|
95
|
+
* flag only — no interview prompt. Without it, an instruction file
|
|
96
|
+
* carrying rows the generator didn't produce causes init to exit 2 with
|
|
97
|
+
* `code: 'instruction-file.foreign-rows'` so unattended upgrade flows
|
|
98
|
+
* surface the would-be data loss instead of silently clobbering it.
|
|
99
|
+
*
|
|
91
100
|
* @param {string[]} args
|
|
92
|
-
* @returns {{ runtimes: string[], runtimes_explicit: boolean, target: string|null, interactive: boolean, skipBootstrap: boolean, visibility?: 'solo'|'team', autonomy?: 0|1|2|3, accept_new_defaults: boolean, yes: boolean, forceRuntimes: boolean }}
|
|
101
|
+
* @returns {{ runtimes: string[], runtimes_explicit: boolean, target: string|null, interactive: boolean, skipBootstrap: boolean, visibility?: 'solo'|'team', autonomy?: 0|1|2|3, accept_new_defaults: boolean, yes: boolean, forceRuntimes: boolean, forceOverwriteCatalog: boolean, keepLegacy: boolean }}
|
|
93
102
|
*/
|
|
94
103
|
export function parseInstallFlags(args) {
|
|
95
104
|
const flagSet = new Set(args.filter((a) => a.startsWith('--')))
|
|
@@ -105,6 +114,14 @@ export function parseInstallFlags(args) {
|
|
|
105
114
|
// 0.6.7 (Bug 2 fix) — opt-in bypass of the upgrade_noop short-circuit
|
|
106
115
|
// so /apt:update can re-stamp runtime files on a no-drift upgrade.
|
|
107
116
|
const forceRuntimes = flagSet.has('--force-runtimes')
|
|
117
|
+
// 0.7.0 (FRAMEWORK-BUG-023) — opt-in bypass of the foreign-row check
|
|
118
|
+
// in injectInstructionFile. Power-user flag only.
|
|
119
|
+
const forceOverwriteCatalog = flagSet.has('--force-overwrite-catalog')
|
|
120
|
+
// 0.7.0 (FRAMEWORK-BUG-021) — opt out of the codex legacy-tree cleanup
|
|
121
|
+
// that fires when `codex` is in the runtime set. Defaults to false so
|
|
122
|
+
// the cleanup runs by default; pass `--keep-legacy` to preserve the
|
|
123
|
+
// orphaned `.agents/skills/` tree on upgrades.
|
|
124
|
+
const keepLegacy = flagSet.has('--keep-legacy')
|
|
108
125
|
|
|
109
126
|
const classFlags = parseClassFlags(args)
|
|
110
127
|
|
|
@@ -112,6 +129,8 @@ export function parseInstallFlags(args) {
|
|
|
112
129
|
target,
|
|
113
130
|
skipBootstrap,
|
|
114
131
|
forceRuntimes,
|
|
132
|
+
forceOverwriteCatalog,
|
|
133
|
+
keepLegacy,
|
|
115
134
|
visibility: classFlags.visibility,
|
|
116
135
|
autonomy: classFlags.autonomy,
|
|
117
136
|
accept_new_defaults: classFlags.accept_new_defaults,
|
|
@@ -1164,6 +1183,27 @@ export async function cmdInit(projectDir, extraArgs = []) {
|
|
|
1164
1183
|
}
|
|
1165
1184
|
}
|
|
1166
1185
|
|
|
1186
|
+
// FRAMEWORK-BUG-021 — sweep the legacy `.agents/skills/` codex tree
|
|
1187
|
+
// BEFORE the install pipeline runs so the install can re-stamp
|
|
1188
|
+
// `.codex/` against a clean slate. Only fires when codex is in the
|
|
1189
|
+
// requested runtime set and `--keep-legacy` was not passed; the
|
|
1190
|
+
// helper itself is conservative (no-op on managed installs, no-op
|
|
1191
|
+
// when no legacy framework files are present).
|
|
1192
|
+
let legacyCleanup = null
|
|
1193
|
+
if (effectiveAnswers.runtimes.includes('codex')) {
|
|
1194
|
+
if (parsedFlags.keepLegacy) {
|
|
1195
|
+
legacyCleanup = { runtime: 'codex', ran: false, reason: '--keep-legacy' }
|
|
1196
|
+
} else {
|
|
1197
|
+
const cleanup = runLegacyCleanup({ targetDir: installTarget })
|
|
1198
|
+
legacyCleanup = { runtime: 'codex', ...cleanup }
|
|
1199
|
+
if (cleanup.ran && cleanup.retained.length > 0) {
|
|
1200
|
+
process.stderr.write(
|
|
1201
|
+
`[apt-tools] legacy_cleanup retained ${cleanup.retained.length} user-added path(s) under .agents/skills/: ${cleanup.retained.join(', ')}\n`,
|
|
1202
|
+
)
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1167
1207
|
const results = await installRuntimes(
|
|
1168
1208
|
effectiveAnswers.runtimes,
|
|
1169
1209
|
canonicalRoot,
|
|
@@ -1186,7 +1226,14 @@ export async function cmdInit(projectDir, extraArgs = []) {
|
|
|
1186
1226
|
effectiveAnswers,
|
|
1187
1227
|
interactiveTty,
|
|
1188
1228
|
kernelResult,
|
|
1229
|
+
parsedFlags.forceOverwriteCatalog === true,
|
|
1230
|
+
legacyCleanup,
|
|
1189
1231
|
)
|
|
1232
|
+
// FRAMEWORK-BUG-023 — finishInit signals the foreign-row defensive
|
|
1233
|
+
// abort by returning an early-exit sentinel envelope.
|
|
1234
|
+
if (envelope?.__earlyExit) {
|
|
1235
|
+
return envelope.__earlyExit
|
|
1236
|
+
}
|
|
1190
1237
|
if (interactiveTty) {
|
|
1191
1238
|
await renderOutro(effectiveAnswers, results, version, kernelResult)
|
|
1192
1239
|
// TTY mode: envelope was persisted to disk + suppressed from stdout
|
|
@@ -1365,6 +1412,19 @@ async function renderOutro(answers, results, version, kernelResult) {
|
|
|
1365
1412
|
* deps/` — surfaced in the envelope so
|
|
1366
1413
|
* downstream tooling can detect a
|
|
1367
1414
|
* degraded install.
|
|
1415
|
+
* @param {boolean} [forceOverwriteCatalog=false]
|
|
1416
|
+
* FRAMEWORK-BUG-023 — bypass the
|
|
1417
|
+
* foreign-row check in
|
|
1418
|
+
* injectInstructionFile when the user
|
|
1419
|
+
* explicitly opted in via
|
|
1420
|
+
* --force-overwrite-catalog.
|
|
1421
|
+
* @param {object|null} [legacyCleanup=null]
|
|
1422
|
+
* FRAMEWORK-BUG-021 — outcome of the
|
|
1423
|
+
* codex `.agents/skills/` cleanup
|
|
1424
|
+
* step. `null` when codex was not in
|
|
1425
|
+
* the runtime set; otherwise the
|
|
1426
|
+
* shape `{ runtime, ran, … }` from
|
|
1427
|
+
* `runLegacyCleanup`.
|
|
1368
1428
|
*/
|
|
1369
1429
|
function finishInit(
|
|
1370
1430
|
targetDir,
|
|
@@ -1376,6 +1436,8 @@ function finishInit(
|
|
|
1376
1436
|
answers,
|
|
1377
1437
|
suppressStdout = false,
|
|
1378
1438
|
kernelResult = null,
|
|
1439
|
+
forceOverwriteCatalog = false,
|
|
1440
|
+
legacyCleanup = null,
|
|
1379
1441
|
) {
|
|
1380
1442
|
writeLastInstallSummary(targetDir, runtimeResults, version)
|
|
1381
1443
|
|
|
@@ -1506,7 +1568,40 @@ function finishInit(
|
|
|
1506
1568
|
if (existsSync(claudeMdPath)) instructionFile = claudeMdPath
|
|
1507
1569
|
else if (existsSync(agentsMdPath)) instructionFile = agentsMdPath
|
|
1508
1570
|
else instructionFile = claudeMdPath
|
|
1509
|
-
|
|
1571
|
+
// FRAMEWORK-BUG-023 — generate the section from discoverSkills before
|
|
1572
|
+
// handing it to the writer. The writer also runs a foreign-row defensive
|
|
1573
|
+
// check; if rows present in the file aren't in the generated set we
|
|
1574
|
+
// signal exit-2 via the __earlyExit sentinel so cmdInit can route the
|
|
1575
|
+
// envelope through exitWith() without writing a malformed install.
|
|
1576
|
+
const aptSection = buildAptSection({
|
|
1577
|
+
canonicalRoot,
|
|
1578
|
+
targetDir,
|
|
1579
|
+
cli: detectAgentRuntime(),
|
|
1580
|
+
})
|
|
1581
|
+
const injectionOutcome = injectInstructionFile(instructionFile, {
|
|
1582
|
+
aptSection,
|
|
1583
|
+
force: forceOverwriteCatalog,
|
|
1584
|
+
})
|
|
1585
|
+
if (injectionOutcome.status === 'aborted-foreign-rows') {
|
|
1586
|
+
return {
|
|
1587
|
+
__earlyExit: exitWith(
|
|
1588
|
+
{
|
|
1589
|
+
ok: false,
|
|
1590
|
+
status: 'error',
|
|
1591
|
+
command: 'init',
|
|
1592
|
+
code: 'instruction-file.foreign-rows',
|
|
1593
|
+
message:
|
|
1594
|
+
`${basename(instructionFile)} contains skill rows not present in the installed framework: ` +
|
|
1595
|
+
`${injectionOutcome.foreignRowIds.join(', ')}. ` +
|
|
1596
|
+
'Re-run with --force-overwrite-catalog to regenerate anyway, or remove the foreign rows manually.',
|
|
1597
|
+
foreignRowIds: injectionOutcome.foreignRowIds,
|
|
1598
|
+
instructionFile: basename(instructionFile),
|
|
1599
|
+
},
|
|
1600
|
+
2,
|
|
1601
|
+
),
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
const injectionResult = injectionOutcome.status
|
|
1510
1605
|
|
|
1511
1606
|
const installedClaude = chosenIds.includes('claude') || existsSync(join(targetDir, '.claude'))
|
|
1512
1607
|
const precompactResult = installedClaude
|
|
@@ -1617,6 +1712,11 @@ function finishInit(
|
|
|
1617
1712
|
...(kernelResult.reason ? { reason: kernelResult.reason } : {}),
|
|
1618
1713
|
}
|
|
1619
1714
|
: null,
|
|
1715
|
+
// FRAMEWORK-BUG-021 — always-present envelope field. `null` when
|
|
1716
|
+
// codex was not in the runtime set; otherwise the cleanup outcome
|
|
1717
|
+
// (uses `{ ran: false, reason }` for every skip case so parsers
|
|
1718
|
+
// see a uniform shape).
|
|
1719
|
+
legacy_cleanup: legacyCleanup,
|
|
1620
1720
|
}
|
|
1621
1721
|
|
|
1622
1722
|
if (suppressStdout) {
|