trackfw 7.1.0 → 7.3.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/package.json +1 -1
- package/src/commands/audit-surface.js +520 -0
- package/src/commands/barrier.js +107 -8
- package/src/commands/commit.js +7 -3
- package/src/commands/doctor.js +17 -3
- package/src/commands/index.js +3 -0
- package/src/commands/integrations.js +80 -1
- package/src/commands/push.js +51 -0
- package/src/commands/release.js +47 -0
- package/src/commands/ship.js +10 -2
- package/src/commands/thirdparty.js +7 -2
- package/src/commands/update-harness.js +4 -1
- package/src/commands/update.js +21 -2
- package/src/config/index.js +96 -2
- package/src/generators/hooks.js +194 -13
- package/src/generators/init.js +84 -16
- package/src/generators/roadmap.js +53 -2
- package/src/integrations/assets/agents/architect.md +17 -3
- package/src/integrations/assets/agents/security.md +3 -0
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/index.js +1 -1
- package/src/integrations/render.js +116 -2
- package/src/integrations/scaffold_doctor.js +366 -0
- package/src/lib/update-engine.js +13 -1
- package/src/push/runner.js +223 -0
- package/src/release/runner.js +474 -0
- package/src/ship/runner.js +177 -21
- package/src/validator/index.js +270 -13
|
@@ -5,6 +5,42 @@ const config = require('../config')
|
|
|
5
5
|
const { localDateISO } = require('./date')
|
|
6
6
|
const { resolveReqFiles } = require('../validator/index.js')
|
|
7
7
|
|
|
8
|
+
// wave0Block — "## Wave 0 — Threat Model" section prepended to every generated roadmap, before
|
|
9
|
+
// the first implementation wave (AC1, AC12). Byte-identical to internal/generators/roadmap.go's
|
|
10
|
+
// wave0Block/wave0GateFence and to pypi/trackfw/generators/roadmap.py's WAVE0_BLOCK (gate:
|
|
11
|
+
// scripts/check-artifact-parity.sh).
|
|
12
|
+
//
|
|
13
|
+
// The gate command (`exit 1`) is fixed, literal and never interpolated with any REQ title, slug
|
|
14
|
+
// or user-controlled string — see the Go generator's comment for the full rationale (AC13,
|
|
15
|
+
// docs/cli-parity.md § "trackfw barrier"). It intentionally fails closed until ML-0A replaces it
|
|
16
|
+
// with a real, project-specific check.
|
|
17
|
+
//
|
|
18
|
+
// The ML is always ML-0A, never ML-1A — newRoadmapFromReq labels MLs derived from REQ acceptance
|
|
19
|
+
// criteria "ML-1A", "ML-1B", ... starting at the first criterion.
|
|
20
|
+
const WAVE0_BLOCK = `## Wave 0 — Threat Model
|
|
21
|
+
> Dependencies: none. Blocks all implementation.
|
|
22
|
+
|
|
23
|
+
### ML-0A — Threat model for this roadmap
|
|
24
|
+
**Status:** pending
|
|
25
|
+
**Files affected:**
|
|
26
|
+
**Actions:**
|
|
27
|
+
1. Enumeration completeness — is the list of surfaces in this roadmap complete? Name what is missing, or show the list is closed. Do not limit the search to the files already named by the REQ — before declaring the list closed, search the repository for other places that emit the same artifact or the same pattern (for example, grep for the literal the final artifact contains).
|
|
28
|
+
2. Threat model — who empties this Wave 0 without breaking any written rule, and how?
|
|
29
|
+
3. Falsification targets in both directions — for each surface, what breaks when the behavior regresses, and what breaks when it regresses the opposite way?
|
|
30
|
+
4. Declared residual — what this design accepts not covering.
|
|
31
|
+
**Acceptance criteria:**
|
|
32
|
+
- [ ] The four sections above answered with evidence, not a one-line assertion
|
|
33
|
+
- [ ] No implementation line written for this ML
|
|
34
|
+
|
|
35
|
+
**Gates da wave:**
|
|
36
|
+
\`\`\`bash
|
|
37
|
+
# Wave 0 gate — replace this placeholder with a project-specific check before
|
|
38
|
+
# marking ML-0A done. Do not remove the gate; replace its command (AC13).
|
|
39
|
+
exit 1 # placeholder gate fails closed until ML-0A replaces it — see docs/cli-parity.md
|
|
40
|
+
\`\`\`
|
|
41
|
+
|
|
42
|
+
`
|
|
43
|
+
|
|
8
44
|
const VALID_STATES = ['backlog', 'analyzing', 'wip', 'blocked', 'done', 'abandoned']
|
|
9
45
|
const STATE_ORDER = ['analyzing', 'wip', 'backlog', 'blocked', 'done', 'abandoned']
|
|
10
46
|
const VALID_STATES_MESSAGE = VALID_STATES.join(', ')
|
|
@@ -400,6 +436,14 @@ function syncReqReferences(movedBasename, newRoadmapPath, cfg) {
|
|
|
400
436
|
* Em modo by_agent, usa o primeiro agente configurado.
|
|
401
437
|
*/
|
|
402
438
|
function newRoadmap(title, reqPath) {
|
|
439
|
+
// AC1/AC2: o título é dado de uma linha — newline e CR são entrada malformada.
|
|
440
|
+
// Mensagem byte-idêntica nos 3 CLIs (docs/cli-parity.md).
|
|
441
|
+
if (/[\n\r]/.test(title)) {
|
|
442
|
+
console.error('Error: roadmap title must be a single line: newline and carriage return are not allowed')
|
|
443
|
+
process.exitCode = 1
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
403
447
|
const cfg = config.load()
|
|
404
448
|
const date = localDateISO()
|
|
405
449
|
const slug = toSlug(title)
|
|
@@ -441,7 +485,7 @@ REQ: ${reqPath || ''}
|
|
|
441
485
|
- [ ]
|
|
442
486
|
- [ ]
|
|
443
487
|
|
|
444
|
-
## Wave 1 — <name> (parallel MLs)
|
|
488
|
+
${WAVE0_BLOCK}## Wave 1 — <name> (parallel MLs)
|
|
445
489
|
> Dependencies: none
|
|
446
490
|
|
|
447
491
|
### ML-1A — ${title}
|
|
@@ -476,6 +520,13 @@ function newRoadmapFromReq(reqPath) {
|
|
|
476
520
|
const basename = path.basename(reqPath)
|
|
477
521
|
const title = parsedTitle || basename.replace(/\.md$/, '').replace(/^REQ-/, '')
|
|
478
522
|
|
|
523
|
+
// AC1: o título lido da REQ também pode conter newline forjado — rejeitar cedo.
|
|
524
|
+
if (/[\n\r]/.test(title)) {
|
|
525
|
+
console.error('Error: roadmap title must be a single line: newline and carriage return are not allowed')
|
|
526
|
+
process.exitCode = 1
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
|
|
479
530
|
const cfg = config.load()
|
|
480
531
|
const date = localDateISO()
|
|
481
532
|
const slug = toSlug(title)
|
|
@@ -509,7 +560,7 @@ function newRoadmapFromReq(reqPath) {
|
|
|
509
560
|
mlLines.push('- [ ] build passes')
|
|
510
561
|
mlLines.push('- [ ] tests green')
|
|
511
562
|
}
|
|
512
|
-
const mlSection = mlLines.join('\n')
|
|
563
|
+
const mlSection = WAVE0_BLOCK + mlLines.join('\n')
|
|
513
564
|
|
|
514
565
|
const adrRef = linkedADR ? `\nADR: ${linkedADR}` : ''
|
|
515
566
|
|
|
@@ -24,16 +24,18 @@ Append an entry to `docs/agents-working-context.md` when you start and when you
|
|
|
24
24
|
Before investigating a bug or unexpected behavior, read `vault/notes/index.md` when it exists and open the related notes. After reaching a non-obvious root cause, write a note and link it in the index. Rule of thumb: if another agent would lose more than ten minutes tomorrow without the note, the note must exist.
|
|
25
25
|
|
|
26
26
|
## Git authority
|
|
27
|
-
`trackfw_architect` is the **only** role with Git authority in this project. No other agent creates branches, commits, pushes, checks out, merges or rebases — every specialist hands its work back to `trackfw_architect` uncommitted. This role: creates the branch with `trackfw branch new <type>/<slug>`, which validates a matching roadmap already sits in `wip/` or `done/` before running `git checkout -b` and blocks with governance orientation if none is found; falls back to a raw `git checkout -b` only if `trackfw branch new` is an unknown command (binary predating v6.4.0 or missing from PATH) or fails for a reason other than the expected missing-roadmap block, while still requiring REQ + roadmap in `wip` first; audits the full diff produced by specialists against the assigned scope; performs every commit, including orchestration artifacts (ADRs, REQs, roadmaps, vault notes, the working context file) and the product code produced by specialists once audited; pushes to the working branch
|
|
27
|
+
`trackfw_architect` is the **only** role with Git authority in this project. No other agent creates branches, commits, pushes, checks out, merges or rebases — every specialist hands its work back to `trackfw_architect` uncommitted. This role: creates the branch with `trackfw branch new <type>/<slug>`, which validates a matching roadmap already sits in `wip/` or `done/` before running `git checkout -b` and blocks with governance orientation if none is found; falls back to a raw `git checkout -b` only if `trackfw branch new` is an unknown command (binary predating v6.4.0 or missing from PATH) or fails for a reason other than the expected missing-roadmap block, while still requiring REQ + roadmap in `wip` first; audits the full diff produced by specialists against the assigned scope; performs every commit, including orchestration artifacts (ADRs, REQs, roadmaps, vault notes, the working context file) and the product code produced by specialists once audited; pushes commits already created to the working branch with `trackfw push`; and suggests opening a pull request, opening it only when the user explicitly asks. Never merge.
|
|
28
|
+
|
|
29
|
+
Three distinct commands, never interchangeable: `trackfw commit` commits staged changes on the working branch; `trackfw push` pushes commits already created — use it instead of a raw `git push`, which the guard intercepts and redirects here; `trackfw ship` composes commit + push + PR into one governed step. Prefer the raw `git commit`/`git push` only when `trackfw commit`/`trackfw push` are unknown commands (binary predating the version that introduced them) or fail for a reason unrelated to governance.
|
|
28
30
|
|
|
29
31
|
## Barrier protocol
|
|
30
|
-
Before releasing the next wave: invoke the `code-quality` and `security` roles whenever the change warrants their review (new code paths, dependencies, permissions, secrets, parsers, or attack surface); block the next wave on any failed check and dispatch a corrective microbatch to the owning role instead of proceeding; audit the diff and re-run the wave's gates before performing any commit. A green `trackfw barrier` result from the CLI is necessary but not sufficient — it does not replace the specialized code-quality/security review or the manual diff audit.
|
|
32
|
+
Before releasing the next wave: confirm Wave 0 (threat model) of the current roadmap is audited — every MB in it, before dispatching any implementation wave; invoke the `code-quality` and `security` roles whenever the change warrants their review (new code paths, dependencies, permissions, secrets, parsers, or attack surface); block the next wave on any failed check and dispatch a corrective microbatch to the owning role instead of proceeding; audit the diff and re-run the wave's gates before performing any commit. A green `trackfw barrier` result from the CLI is necessary but not sufficient — it does not replace the specialized code-quality/security review or the manual diff audit.
|
|
31
33
|
|
|
32
34
|
## Parallelization
|
|
33
35
|
Analyze real dependencies between microbatches before assigning work. Microbatches touching disjoint files run in parallel; microbatches sharing any file — including generated trees, build outputs and the git index — become sequential, and the reason is documented. Put an explicit barrier between waves. Every handoff prompt must be self-contained: exact files, exact values, exact commands. Never let two agents edit the same file at the same time.
|
|
34
36
|
|
|
35
37
|
## Workflow
|
|
36
|
-
Analyze the codebase and requirements; record material decisions in an ADR; create the REQ with an explicit negative scope; produce a roadmap of waves and microbatches with measurable acceptance criteria; create the branch; commit the governance artifacts before any handoff; dispatch the
|
|
38
|
+
Analyze the codebase and requirements; record material decisions in an ADR; create the REQ with an explicit negative scope; produce a roadmap of waves and microbatches with measurable acceptance criteria, starting with a Wave 0 threat model (`security` role, `trackfw barrier <roadmap> --wave 0`) that MUST be audited before any implementation wave is dispatched; create the branch; commit the governance artifacts before any handoff; dispatch Wave 0 first, then the implementation waves; audit each microbatch against its acceptance criteria; update the roadmap; open the pull request only on request.
|
|
37
39
|
|
|
38
40
|
## Dispatch contract
|
|
39
41
|
Naming a specialist in prose or in a roadmap's `squad:` field is documentation, not delegation — it does not route the Agent tool call by itself. Every dispatch to a specialist MUST pass the Agent tool's `subagent_type` parameter explicitly; omitting it silently falls back to the generic `general-purpose` agent, which has none of the intended specialist's domain instructions. The correct `subagent_type` value is the `name:` from the frontmatter of that role's installed agent file — always `<slug>-tf`, where `<slug>` depends on the identity the user configured (Greek, Norse, custom, or otherwise); never assume a fixed name. If the exact value is not already known, read the installed agent file for that role before dispatching instead of guessing from the name used in prose. Confirm `subagent_type` is present and correct before every dispatch call.
|
|
@@ -44,4 +46,16 @@ Before releasing the next wave, verify each acceptance criterion yourself: read
|
|
|
44
46
|
## Mission
|
|
45
47
|
Map the existing architecture and traceability chain before proposing changes. Record material decisions as ADRs, produce decision-complete plans, and delegate implementation to the appropriate specialist. Do not implement product code.
|
|
46
48
|
|
|
49
|
+
## Response length
|
|
50
|
+
|
|
51
|
+
Default: what changed · what I decided · what I need from you. Three to five lines.
|
|
52
|
+
|
|
53
|
+
Scale up only on these three triggers, and only on them: a **blocker** that stops the next wave; a **pending user decision** that cannot be inferred from context; an **error I made** that cannot be self-corrected.
|
|
54
|
+
|
|
55
|
+
Never cut, even when short: measured evidence (command and result), barrier verdict, decision taken and why. A response that buries a blocker in paragraph seven produced the same effect as not reporting it.
|
|
56
|
+
|
|
57
|
+
Cut: restating what an executor already reported, re-explaining reasoning already given, recapping state that has not changed, closing praise. Tables and code blocks only when they replace prose, never when they add to it.
|
|
58
|
+
|
|
59
|
+
Depth is on demand from the user.
|
|
60
|
+
|
|
47
61
|
— Architect, Principal Software Architect
|
|
@@ -42,6 +42,9 @@ This role never executes Git operations — no `branch`, `commit`, `push`, `chec
|
|
|
42
42
|
## Definition of done
|
|
43
43
|
Green build and tests do not close a microbatch. It is done when the roadmap reflects the new status and the governance artifacts sit in the correct state folder. Leaving an artifact in the wrong folder is the failure the gate exists to catch.
|
|
44
44
|
|
|
45
|
+
## Wave 0 deliverable
|
|
46
|
+
When dispatched for a roadmap's `## Wave 0 — Threat Model`, produce a written threat model with exactly four sections, before any implementation line is written: **enumeration completeness** (is the list of surfaces in the roadmap complete? name what is missing, or show the list is closed); **threat model** (who empties this Wave 0 without breaking any written rule, and how — the adversary here is the rushed implementer and the optimistic architect, not an external attacker); **falsification targets in both directions** (for each surface: where the sabotage enters, which gate should catch it, in which direction — false negative and false positive); **declared residual** (what this design accepts not covering, said plainly). Mark each of the four sections done with evidence, not a one-line assertion — `trackfw barrier <roadmap> --wave 0` checks that the ML is `✅` and its acceptance criteria are `[x]`, never the quality of the analysis; a shallow Wave 0 can still pass those two checks, so the `trackfw_architect` audit of the artifact's content is what actually catches it.
|
|
47
|
+
|
|
45
48
|
## Mission
|
|
46
49
|
Perform evidence-backed threat analysis. Prioritize concrete exploit paths and mitigations, preserve authentication and least privilege, and never expose secrets or weaken controls to make a test pass.
|
|
47
50
|
|
|
Binary file
|
|
@@ -56,7 +56,7 @@ function buildPlans(kind, options = {}) {
|
|
|
56
56
|
for (const item of selected.itemEntries) {
|
|
57
57
|
for (const installPath of paths) {
|
|
58
58
|
const destination = installPath.path.replace('{{id}}', item.id)
|
|
59
|
-
let content = render({ target: targetEntry.id, kind, item, content: readAsset(item), capability, destination, identity: identityConfig })
|
|
59
|
+
let content = render({ target: targetEntry.id, kind, item, content: readAsset(item), capability, destination, identity: identityConfig, agentModels: options.agentModels || {} })
|
|
60
60
|
// D5/D9 extension point: reproduce any persisted third-party
|
|
61
61
|
// reference block so regenerating this exact artifact (e.g. a
|
|
62
62
|
// later `trackfw agents update`) settles at state "current"
|
|
@@ -97,6 +97,19 @@ function mapModelCursor(model) {
|
|
|
97
97
|
return CURSOR_MODEL_MAP[model] || ''
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// containsControlChar reports whether s contains any ASCII control character
|
|
101
|
+
// (U+0000–U+001F) or a Unicode line/paragraph separator (U+2028, U+2029).
|
|
102
|
+
// Mirrors internal/integrations/render.go:containsControlChar (ML-5C).
|
|
103
|
+
// In JavaScript, strings are UTF-16; U+2028/U+2029 are single code units
|
|
104
|
+
// accessible via charCodeAt, so no surrogate-pair handling is needed.
|
|
105
|
+
function containsControlChar(s) {
|
|
106
|
+
for (let i = 0; i < s.length; i++) {
|
|
107
|
+
const c = s.charCodeAt(i)
|
|
108
|
+
if (c < 0x20 || c === 0x2028 || c === 0x2029) return true
|
|
109
|
+
}
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
100
113
|
// rewriteFrontmatterModelLine substitui a linha "model:" do frontmatter de um
|
|
101
114
|
// markdown cru por value, preservando toda outra linha do frontmatter e o
|
|
102
115
|
// corpo byte a byte. Se o frontmatter não tiver linha "model:", uma é
|
|
@@ -105,7 +118,14 @@ function mapModelCursor(model) {
|
|
|
105
118
|
// espelha a detecção de fronteira usada por rewriteFrontmatterFields,
|
|
106
119
|
// escopada à chave única "model". Espelha
|
|
107
120
|
// internal/integrations/render.go:rewriteFrontmatterModelLine.
|
|
121
|
+
//
|
|
122
|
+
// Throws an Error if value contains any ASCII control character (U+0000–U+001F).
|
|
123
|
+
// Model IDs never require control characters; any such value is rejected to
|
|
124
|
+
// prevent frontmatter injection (ML-5A).
|
|
108
125
|
function rewriteFrontmatterModelLine(source, value) {
|
|
126
|
+
if (containsControlChar(value)) {
|
|
127
|
+
throw new Error(`model value contains control character and was rejected: model IDs never require newlines or other control characters (got ${JSON.stringify(value)})`)
|
|
128
|
+
}
|
|
109
129
|
const trimmed = String(source).trim()
|
|
110
130
|
if (!trimmed.startsWith('---\n')) return trimmed
|
|
111
131
|
const end = trimmed.indexOf('\n---', 4)
|
|
@@ -296,6 +316,27 @@ function frontmatterName(content) {
|
|
|
296
316
|
return undefined
|
|
297
317
|
}
|
|
298
318
|
|
|
319
|
+
// isVersionString reports whether s is a bare version string (digits and dots
|
|
320
|
+
// only, e.g. "5", "4.6", "1.0.2"). Mirrors internal/integrations/render.go:isVersionString.
|
|
321
|
+
// Values that don't match — pre-composed IDs like "claude-sonnet-4-5-20250929" (hyphens),
|
|
322
|
+
// "latest" (letters), or "" (empty) — return false and the caller uses the value literally
|
|
323
|
+
// (escape hatch, ADR-2026-08-21 §3).
|
|
324
|
+
function isVersionString(s) {
|
|
325
|
+
if (!s) return false
|
|
326
|
+
return /^[0-9]+(\.[0-9]+)*$/.test(s)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// composeClaudeModelID builds a Claude model identifier from tier and version,
|
|
330
|
+
// applying the three composition rules from the official Anthropic docs.
|
|
331
|
+
// Mirrors internal/integrations/render.go:composeClaudeModelID.
|
|
332
|
+
// Rule 1: dots become hyphens ("4.6" → "claude-sonnet-4-6")
|
|
333
|
+
// Rule 2: major-only version never gets "-0" ("5" → "claude-sonnet-5")
|
|
334
|
+
// Rule 3: handled via escape hatch — pre-composed IDs are never version strings
|
|
335
|
+
function composeClaudeModelID(tier, version) {
|
|
336
|
+
const hyphenated = version.replace(/\./g, '-')
|
|
337
|
+
return `claude-${tier}-${hyphenated}`
|
|
338
|
+
}
|
|
339
|
+
|
|
299
340
|
// render converte um item canônico do catálogo para a representação nativa
|
|
300
341
|
// declarada por uma surface alvo. Quando identity carrega uma identidade
|
|
301
342
|
// customizada para item.id, o name/description/body renderizados são
|
|
@@ -321,7 +362,12 @@ function frontmatterName(content) {
|
|
|
321
362
|
// normalize(content) — a mesma expressão usada antes de existir suporte a
|
|
322
363
|
// identidade — então a saída sem identidade é garantida byte a byte
|
|
323
364
|
// inalterada por construção, não por coincidência.
|
|
324
|
-
|
|
365
|
+
//
|
|
366
|
+
// agentModels (opcional) — objeto de tier→versão lido de trackfw.yaml's agent_models.
|
|
367
|
+
// Ausente ou vazio: saída idêntica a antes. Somente o alvo "claude" recebe IDs
|
|
368
|
+
// compostos; Codex, Cursor, Antigravity, OpenCode, Gemini, Kiro não são afetados
|
|
369
|
+
// (ADR-2026-08-21 §4 — gate, não cuidado).
|
|
370
|
+
function render({ kind, content, capability, item, identity: cfg, target, agentModels }) {
|
|
325
371
|
if (kind === 'skills') return normalize(content)
|
|
326
372
|
|
|
327
373
|
const id = item && cfg ? lookupIdentity(cfg, item.id) : undefined
|
|
@@ -407,6 +453,18 @@ function render({ kind, content, capability, item, identity: cfg, target }) {
|
|
|
407
453
|
if (target === 'cursor') {
|
|
408
454
|
const mappedModel = mapModelCursor(parts.model)
|
|
409
455
|
source = mappedModel ? rewriteFrontmatterModelLine(source, mappedModel) : removeFrontmatterModelLine(source)
|
|
456
|
+
} else if (target === 'claude' && agentModels && Object.keys(agentModels).length > 0) {
|
|
457
|
+
// Allowlist: only the "claude" target receives composed model IDs.
|
|
458
|
+
// Codex, Cursor, Antigravity, OpenCode, Gemini, Kiro and any other target are left
|
|
459
|
+
// untouched even when agentModels is populated (ADR-2026-08-21 §4 — gate, not cuidado).
|
|
460
|
+
const version = agentModels[parts.model]
|
|
461
|
+
if (version) {
|
|
462
|
+
const modelID = isVersionString(version)
|
|
463
|
+
? composeClaudeModelID(parts.model, version)
|
|
464
|
+
: version // escape hatch: non-version value used literally (ADR-2026-08-21 §3)
|
|
465
|
+
source = rewriteFrontmatterModelLine(source, modelID)
|
|
466
|
+
}
|
|
467
|
+
// Empty string version means "no pin": leave source unchanged (tier alias preserved).
|
|
410
468
|
}
|
|
411
469
|
if (!id) return normalize(source)
|
|
412
470
|
const withBody = insertBodyPrefix(source, greeting)
|
|
@@ -415,4 +473,60 @@ function render({ kind, content, capability, item, identity: cfg, target }) {
|
|
|
415
473
|
return normalize(withSignature)
|
|
416
474
|
}
|
|
417
475
|
|
|
418
|
-
|
|
476
|
+
// resolveAgentModel returns the model string that render() would write to the
|
|
477
|
+
// model field of an artifact with the given representation and targetID, for
|
|
478
|
+
// an agent with the given tier. Mirrors
|
|
479
|
+
// internal/integrations/models.go:ResolveAgentModel.
|
|
480
|
+
//
|
|
481
|
+
// Returns { resolved: string, present: boolean }.
|
|
482
|
+
// present=false means the artifact format omits the model field entirely — the
|
|
483
|
+
// caller should display "—" or equivalent rather than the tier alias.
|
|
484
|
+
//
|
|
485
|
+
// agentModels applies only for the "claude" target (ADR-2026-08-21 §4).
|
|
486
|
+
function resolveAgentModel(tier, representation, targetID, agentModels) {
|
|
487
|
+
if (representation === 'custom-agent-toml') {
|
|
488
|
+
const v = resolveModelCodex(tier)
|
|
489
|
+
return { resolved: v, present: v !== '' }
|
|
490
|
+
}
|
|
491
|
+
if (representation === 'cli-agent-json' || representation === 'agent-json') {
|
|
492
|
+
return { resolved: '', present: false }
|
|
493
|
+
}
|
|
494
|
+
if (representation === 'agent-directory') {
|
|
495
|
+
const v = resolveModel(tier)
|
|
496
|
+
return { resolved: v, present: v !== '' }
|
|
497
|
+
}
|
|
498
|
+
if (representation === 'opencode-agent') {
|
|
499
|
+
return { resolved: '', present: false }
|
|
500
|
+
}
|
|
501
|
+
// default branch — mirrors render()'s default case
|
|
502
|
+
if (targetID === 'cursor') {
|
|
503
|
+
const v = mapModelCursor(tier)
|
|
504
|
+
return { resolved: v, present: v !== '' }
|
|
505
|
+
}
|
|
506
|
+
if (targetID === 'claude') {
|
|
507
|
+
const am = agentModels || {}
|
|
508
|
+
const version = am[tier]
|
|
509
|
+
if (version) {
|
|
510
|
+
const modelID = isVersionString(version) ? composeClaudeModelID(tier, version) : version
|
|
511
|
+
return { resolved: modelID, present: true }
|
|
512
|
+
}
|
|
513
|
+
// no pin → tier alias unchanged
|
|
514
|
+
}
|
|
515
|
+
return { resolved: tier, present: true }
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// looksLikeSuspectModelValue reports whether v is an agent_models value that
|
|
519
|
+
// will trigger the escape-hatch path and likely produce an invalid model
|
|
520
|
+
// identifier in the rendered artifact. Returns true when v is not a bare
|
|
521
|
+
// version string AND does not start with "claude-". Callers should emit a
|
|
522
|
+
// per-tier warning to stderr (not per-row) when this returns true.
|
|
523
|
+
// Mirrors internal/integrations/models.go:LooksLikeSuspectModelValue.
|
|
524
|
+
// looksLikeSuspectModelValue mirrors internal/integrations/models.go:LooksLikeSuspectModelValue.
|
|
525
|
+
// ML-5A: values with control characters are always suspect — rewriteFrontmatterModelLine
|
|
526
|
+
// rejects them outright, so this function must agree with the write path to keep the
|
|
527
|
+
// "agents models" inspection command aligned with "agents install/update" behavior.
|
|
528
|
+
function looksLikeSuspectModelValue(v) {
|
|
529
|
+
return containsControlChar(v) || (!isVersionString(v) && !v.startsWith('claude-'))
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
module.exports = { render, markdownParts, frontmatterName, greetingLine, insertBodyPrefix, rewriteFrontmatterFields, rewriteFrontmatterModelLine, rewriteSignatureLine, isVersionString, composeClaudeModelID, resolveAgentModel, looksLikeSuspectModelValue }
|