@aperant/framework 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/agents/{apt-pr-review-xplatform.md → apt-pr-review-crossplatform.md} +1 -1
  3. package/dist/cli/commands/init.d.mts.map +1 -1
  4. package/dist/cli/commands/init.mjs +30 -1
  5. package/dist/cli/commands/init.mjs.map +1 -1
  6. package/dist/cli/commands/pr-review/detect-specialists.d.mts +5 -5
  7. package/dist/cli/commands/pr-review/detect-specialists.d.mts.map +1 -1
  8. package/dist/cli/commands/pr-review/detect-specialists.mjs +49 -45
  9. package/dist/cli/commands/pr-review/detect-specialists.mjs.map +1 -1
  10. package/dist/cli/commands/spar-resolve-partner.d.mts +9 -0
  11. package/dist/cli/commands/spar-resolve-partner.d.mts.map +1 -0
  12. package/dist/cli/commands/spar-resolve-partner.mjs +82 -0
  13. package/dist/cli/commands/spar-resolve-partner.mjs.map +1 -0
  14. package/dist/cli/config/gitignore-drift.d.mts.map +1 -1
  15. package/dist/cli/config/gitignore-drift.mjs +1 -0
  16. package/dist/cli/config/gitignore-drift.mjs.map +1 -1
  17. package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
  18. package/dist/cli/config/post-merge-sweep.mjs +1 -0
  19. package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
  20. package/dist/cli/dispatch.d.mts.map +1 -1
  21. package/dist/cli/dispatch.mjs +2 -0
  22. package/dist/cli/dispatch.mjs.map +1 -1
  23. package/dist/cli/help.d.mts.map +1 -1
  24. package/dist/cli/help.mjs +1 -0
  25. package/dist/cli/help.mjs.map +1 -1
  26. package/dist/cli/host/codex-config.d.mts +78 -0
  27. package/dist/cli/host/codex-config.d.mts.map +1 -0
  28. package/dist/cli/host/codex-config.mjs +169 -0
  29. package/dist/cli/host/codex-config.mjs.map +1 -0
  30. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  31. package/dist/cli/install/legacy-paths.mjs +1 -0
  32. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  33. package/dist/cli/wfrun/run.d.mts.map +1 -1
  34. package/dist/cli/wfrun/run.mjs +1 -0
  35. package/dist/cli/wfrun/run.mjs.map +1 -1
  36. package/dist/plugin/.claude-plugin/plugin.json +2 -2
  37. package/dist/plugin/agents/{apt-pr-review-xplatform.md → apt-pr-review-crossplatform.md} +1 -1
  38. package/dist/plugin/skills/apt-pr-review/SKILL.md +13 -13
  39. package/dist/plugin/skills/apt-spar/SKILL.md +14 -6
  40. package/package.json +1 -1
  41. package/skills/apt-pr-review/SKILL.md +13 -13
  42. package/skills/apt-spar/SKILL.md +14 -6
  43. package/src/cli/commands/init.mjs +29 -0
  44. package/src/cli/commands/pr-review/detect-specialists.mjs +51 -45
  45. package/src/cli/commands/spar-resolve-partner.mjs +85 -0
  46. package/src/cli/config/gitignore-drift.mjs +1 -0
  47. package/src/cli/config/post-merge-sweep.mjs +1 -0
  48. package/src/cli/dispatch.mjs +2 -0
  49. package/src/cli/help.mjs +1 -0
  50. package/src/cli/host/codex-config.mjs +165 -0
  51. package/src/cli/install/legacy-paths.mjs +1 -0
  52. package/src/cli/wfrun/run.mjs +1 -0
  53. /package/prompts/github/{pr_xplatform_agent.md → pr_crossplatform_agent.md} +0 -0
@@ -86,6 +86,7 @@ import { cmdRiskCheck } from './commands/risk-check.mjs'
86
86
  import { cmdRoadmap } from './commands/roadmap.mjs'
87
87
  import { cmdRoute } from './commands/route.mjs'
88
88
  import { cmdSeed } from './commands/seed.mjs'
89
+ import { cmdSparResolvePartner } from './commands/spar-resolve-partner.mjs'
89
90
  import { cmdStandards } from './commands/standards.mjs'
90
91
  import { cmdConfig, cmdState, cmdStateArtifacts, cmdStateUpdate } from './commands/state.mjs'
91
92
  import { cmdStatusline } from './commands/statusline.mjs'
@@ -285,6 +286,7 @@ export const DISPATCH = {
285
286
  'author-skill': (args) => wrap(cmdAuthorSkill)(args[1], args.slice(2)),
286
287
  'toolchain-detect': (args) => wrap(cmdToolchainDetect)(args[1], args.slice(2)),
287
288
  'host-detect': () => wrap(cmdHostDetect)(),
289
+ 'spar-resolve-partner': (args) => wrap(cmdSparResolvePartner)(args.slice(1)),
288
290
  reconcile: (args) => wrap(cmdReconcile)(args[1], args.slice(2)),
289
291
  standards: (args) => wrap(cmdStandards)(args[1], args.slice(2)),
290
292
  'precompact-hook': (args) => wrap(cmdPrecompactHook)(args[1]),
package/src/cli/help.mjs CHANGED
@@ -110,6 +110,7 @@ Commands:
110
110
  toolchain-detect <project-dir> Detect project toolchain (linter, formatter, typecheck, test, build)
111
111
  health-check <project-dir> Detect dirty state, stale locks, orphaned events, version drift
112
112
  host-detect Detect which AI CLI is running and its capability matrix
113
+ spar-resolve-partner [--partner codex|claude|gemini] apt:spar live partner-model resolution — reads ~/.codex/config.toml LIVE (fail-closed, no baked default); codex returns {available,model,source,reason} + a transport-walk preference, claude/gemini return self_resolving
113
114
  reconcile <project-dir> Resolve orchestration preferences against host capabilities
114
115
 
115
116
  migrate-pr-reviews <project-dir> Migrate legacy .aperant/reviews/ → .aperant/pr-reviews/pr-{N}/rounds/{R}/ (idempotent)
@@ -0,0 +1,165 @@
1
+ /**
2
+ * host/codex-config.mjs — live codex model resolution for `apt:spar` (Decision B + C).
3
+ *
4
+ * Aperant holds ZERO model names. The only source of truth for the codex
5
+ * partner's model is the user's live `~/.codex/config.toml`, read at invocation
6
+ * time with NO fallback default — mirroring the app's "Model selection —
7
+ * resolver-only, provider-is-output, LIVE" rule.
8
+ *
9
+ * Two exports:
10
+ * - `parseTopLevelCodexModel(tomlText)` — a deterministic top-level TOML
11
+ * `model = "<value>"` line parser (ID-04: top-level only; `[profiles.*]`
12
+ * sections are inactive without `--profile`, which spar never passes).
13
+ * - `resolveCodexSparModel({ home, existsSync, readFileSync })` — reads
14
+ * `<home>/.codex/config.toml` LIVE each call and returns a fail-closed
15
+ * `{ available, model, source, reason }` envelope. NEVER a baked constant.
16
+ *
17
+ * Parser hardening (plan-gate spar refinement #1). The bias rule is
18
+ * fail-closed: when a value is ambiguous, return `reason: 'unparseable'` rather
19
+ * than a wrong or truncated model. A too-strict parse that loses the MCP rung
20
+ * (falling through to the self-resolving plugin/CLI rungs) is acceptable; a
21
+ * too-loose parse that passes a wrong model is a bug.
22
+ */
23
+
24
+ import { join } from 'node:path'
25
+
26
+ /** Human-facing source label the SKILL + envelope reference. */
27
+ export const CODEX_CONFIG_SOURCE = '~/.codex/config.toml'
28
+
29
+ /**
30
+ * @typedef {Object} ParsedCodexModel
31
+ * @property {string|null} model — the resolved top-level model, or null.
32
+ * @property {null|'no_model_key'|'unparseable'} reason — null when a model
33
+ * resolved; a machine reason otherwise.
34
+ */
35
+
36
+ /**
37
+ * Parse the FIRST top-level `model = "<value>"` assignment from codex
38
+ * `config.toml` text. Scans line-by-line, stops at the first `[section]`
39
+ * header (top-level scope ends there), skips comment lines, and resolves the
40
+ * first top-level `model` key.
41
+ *
42
+ * Handles (refinement #1): a leading UTF-8 BOM, an inline `# comment` after the
43
+ * value, a no-space `model="x"` assignment, single-quoted TOML literal strings,
44
+ * and commented-out `#model = "x"` keys (skipped). A malformed / unterminated /
45
+ * escaped-quote value returns `reason: 'unparseable'` — never a truncated
46
+ * string. The first top-level `model` line is decisive: if it is malformed the
47
+ * parser returns `unparseable` rather than scanning for a later valid line
48
+ * (TOML forbids duplicate keys, and a later-line search would be too loose).
49
+ *
50
+ * @param {string} tomlText
51
+ * @returns {ParsedCodexModel}
52
+ */
53
+ export function parseTopLevelCodexModel(tomlText) {
54
+ if (typeof tomlText !== 'string') return { model: null, reason: 'unparseable' }
55
+
56
+ // Strip a leading UTF-8 BOM so `model = "x"` still resolves.
57
+ let text = tomlText
58
+ if (text.charCodeAt(0) === 0xfeff) text = text.slice(1)
59
+
60
+ const lines = text.split(/\r?\n/)
61
+ for (const rawLine of lines) {
62
+ const line = rawLine.trimStart()
63
+ // A `[section]` (or `[[array.of.tables]]`) header ends the top-level
64
+ // scope — stop scanning; a model below this point is profile-scoped.
65
+ if (line.startsWith('[')) break
66
+ // Comment lines (incl. a commented-out `#model = "x"`) are skipped.
67
+ if (line.startsWith('#')) continue
68
+ // Top-level `model` assignment? The key must be EXACTLY `model` —
69
+ // `\s*=` rejects `model_provider`, `models`, `mcp_model`, etc.
70
+ const assign = line.match(/^model\s*=\s*(.*)$/)
71
+ if (!assign) continue
72
+ // First top-level model line is decisive (fail-closed on malformed).
73
+ return parseQuotedValue(assign[1])
74
+ }
75
+ return { model: null, reason: 'no_model_key' }
76
+ }
77
+
78
+ /**
79
+ * Parse a quoted TOML string value (the RHS of `model =`). Accepts a
80
+ * double-quoted basic string or a single-quoted literal string, each optionally
81
+ * followed by whitespace and/or an inline `# comment`. Any other shape
82
+ * (unquoted, unterminated, escaped, empty, or trailing non-comment junk) is
83
+ * fail-closed `unparseable`.
84
+ *
85
+ * @param {string} value — the text after `model\s*=\s*`.
86
+ * @returns {ParsedCodexModel}
87
+ */
88
+ function parseQuotedValue(value) {
89
+ const first = value[0]
90
+ if (first === '"') {
91
+ const m = value.match(/^"([^"\n]*)"(.*)$/)
92
+ if (!m) return { model: null, reason: 'unparseable' } // unterminated
93
+ const inner = m[1]
94
+ // A backslash means TOML escapes are in play — do NOT attempt to
95
+ // unescape; fail closed rather than risk a wrong/truncated model.
96
+ if (inner.includes('\\')) return { model: null, reason: 'unparseable' }
97
+ if (!isOnlyTrailingComment(m[2])) return { model: null, reason: 'unparseable' }
98
+ if (inner.length === 0) return { model: null, reason: 'unparseable' }
99
+ return { model: inner, reason: null }
100
+ }
101
+ if (first === "'") {
102
+ // TOML literal strings carry no escapes.
103
+ const m = value.match(/^'([^'\n]*)'(.*)$/)
104
+ if (!m) return { model: null, reason: 'unparseable' }
105
+ const inner = m[1]
106
+ if (!isOnlyTrailingComment(m[2])) return { model: null, reason: 'unparseable' }
107
+ if (inner.length === 0) return { model: null, reason: 'unparseable' }
108
+ return { model: inner, reason: null }
109
+ }
110
+ // Unquoted / bare value — codex `model` is always a quoted string; bias
111
+ // fail-closed so a malformed line never yields a wrong model.
112
+ return { model: null, reason: 'unparseable' }
113
+ }
114
+
115
+ /**
116
+ * The text after a closing quote must be empty, whitespace, or an inline
117
+ * `# comment`. Anything else is malformed → fail closed.
118
+ *
119
+ * @param {string} rest
120
+ * @returns {boolean}
121
+ */
122
+ function isOnlyTrailingComment(rest) {
123
+ const t = rest.trimStart()
124
+ return t === '' || t.startsWith('#')
125
+ }
126
+
127
+ /**
128
+ * @typedef {Object} CodexSparModelResolution
129
+ * @property {boolean} available — true iff a live top-level model resolved.
130
+ * @property {string|null} model — the resolved model, or null when unavailable.
131
+ * @property {string} source — the config source label (`~/.codex/config.toml`).
132
+ * @property {null|'config_missing'|'no_model_key'|'unparseable'} reason
133
+ */
134
+
135
+ /**
136
+ * Read `<home>/.codex/config.toml` LIVE and resolve the top-level model.
137
+ * Fail-closed (Decision C / ID-03): a missing file, a missing top-level `model`
138
+ * key, or an unparseable value returns `available: false` with a machine reason
139
+ * and `model: null`. It NEVER substitutes a default. There is no module-level
140
+ * cached value — the file is re-read on every call.
141
+ *
142
+ * `existsSync` / `readFileSync` are injected so tests stay hermetic (no real
143
+ * `$HOME` read).
144
+ *
145
+ * @param {{ home: string, existsSync: (p: string) => boolean, readFileSync: (p: string, enc: string) => string }} deps
146
+ * @returns {CodexSparModelResolution}
147
+ */
148
+ export function resolveCodexSparModel({ home, existsSync, readFileSync }) {
149
+ const source = CODEX_CONFIG_SOURCE
150
+ const configPath = join(home, '.codex', 'config.toml')
151
+ if (!existsSync(configPath)) {
152
+ return { available: false, model: null, source, reason: 'config_missing' }
153
+ }
154
+ let text
155
+ try {
156
+ text = readFileSync(configPath, 'utf-8')
157
+ } catch {
158
+ return { available: false, model: null, source, reason: 'config_missing' }
159
+ }
160
+ const { model, reason } = parseTopLevelCodexModel(text)
161
+ if (model) {
162
+ return { available: true, model, source, reason: null }
163
+ }
164
+ return { available: false, model: null, source, reason: reason ?? 'unparseable' }
165
+ }
@@ -181,6 +181,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
181
181
  'agents/apt-improver.md',
182
182
  'agents/apt-planner.md',
183
183
  'agents/apt-pr-review-codebase-fit.md',
184
+ 'agents/apt-pr-review-crossplatform.md',
184
185
  'agents/apt-pr-review-fixer.md',
185
186
  'agents/apt-pr-review-i18n.md',
186
187
  'agents/apt-pr-review-logic.md',
@@ -66,6 +66,7 @@ const TOOL_COMMAND_ALLOWLIST = new Set([
66
66
  'audit',
67
67
  'toolchain-detect',
68
68
  'host-detect',
69
+ 'spar-resolve-partner',
69
70
  'reconcile',
70
71
  'team',
71
72
  'active',