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.
@@ -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; and suggests opening a pull request, opening it only when the user explicitly asks. Never merge.
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 wave; audit each microbatch against its acceptance criteria; update the roadmap; open the pull request only on request.
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
- function render({ kind, content, capability, item, identity: cfg, target }) {
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
- module.exports = { render, markdownParts, frontmatterName, greetingLine, insertBodyPrefix, rewriteFrontmatterFields, rewriteSignatureLine }
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 }