trackfw 6.3.1 → 6.4.1
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 +2 -2
- package/src/branch/runner.js +140 -0
- package/src/commands/branch.js +46 -0
- package/src/commands/discover.js +6 -0
- package/src/commands/index.js +1 -0
- package/src/commands/init.js +3 -2
- package/src/generators/hooks.js +3 -2
- package/src/integrations/assets/agents/architect.md +4 -1
- package/src/integrations/assets/catalog.json +1 -0
- package/src/integrations/render.js +32 -0
- package/src/validator/index.js +48 -13
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "6.
|
|
4
|
-
"description": "Governance CLI for AI-native software delivery and AI coding agents. Enforces traceability: ADR → REQ → ROADMAP → kanban. Native support for Codex, Claude Code, Gemini CLI, Cursor, Copilot, Windsurf,
|
|
3
|
+
"version": "6.4.1",
|
|
4
|
+
"description": "Governance CLI for AI-native software delivery and AI coding agents. Enforces traceability: ADR → REQ → ROADMAP → kanban. Native support for Codex, Claude Code, Gemini CLI, Cursor, Copilot, Windsurf, Amazon Q, OpenCode, and Kiro.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"adr",
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* branch/runner.js — Core implementation of `trackfw branch new`.
|
|
5
|
+
*
|
|
6
|
+
* Moves the branch_has_wip_roadmap governance gate (already enforced by `trackfw validate` and
|
|
7
|
+
* `trackfw ship`) to before branch creation, instead of after. Mirrors internal/commands/branch.go
|
|
8
|
+
* byte-for-byte in behavior and message text — Go is the behavioral reference
|
|
9
|
+
* (docs/cli-parity.md: "Go is the behavioral reference").
|
|
10
|
+
*
|
|
11
|
+
* All git write operations are injectable for testability — never runs a real `git checkout -b`
|
|
12
|
+
* in unit tests.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { spawnSync } = require('child_process')
|
|
16
|
+
const config = require('../config')
|
|
17
|
+
const validator = require('../validator')
|
|
18
|
+
|
|
19
|
+
// branchValidTypes mirrors the vocabulary already enforced by `trackfw ship` (feat|fix|refactor).
|
|
20
|
+
const branchValidTypes = new Set(['feat', 'fix', 'refactor'])
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* parseBranchSpec splits "<type>/<slug>" and validates both parts. type must be one of
|
|
24
|
+
* feat, fix, refactor (branchValidTypes); slug must be non-empty.
|
|
25
|
+
* @param {string} spec
|
|
26
|
+
* @returns {{ branchType: string, slug: string }}
|
|
27
|
+
* @throws {Error} when spec is malformed
|
|
28
|
+
*/
|
|
29
|
+
function parseBranchSpec(spec) {
|
|
30
|
+
const value = String(spec)
|
|
31
|
+
const idx = value.indexOf('/')
|
|
32
|
+
if (idx === -1 || value.slice(0, idx) === '') {
|
|
33
|
+
throw new Error(`invalid branch spec "${spec}" — expected <type>/<slug> with type in feat, fix, refactor`)
|
|
34
|
+
}
|
|
35
|
+
const branchType = value.slice(0, idx)
|
|
36
|
+
const slug = value.slice(idx + 1)
|
|
37
|
+
if (!branchValidTypes.has(branchType)) {
|
|
38
|
+
throw new Error(`invalid branch type "${branchType}" — must be one of feat, fix, refactor`)
|
|
39
|
+
}
|
|
40
|
+
if (slug.trim() === '') {
|
|
41
|
+
throw new Error(`branch slug is required — expected <type>/<slug>, got "${spec}"`)
|
|
42
|
+
}
|
|
43
|
+
return { branchType, slug }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* defaultGitCheckout runs `git checkout -b <branchName>` with inherited stdio, so Git's own output
|
|
48
|
+
* (including branch-already-exists errors) reaches the user unmodified.
|
|
49
|
+
*
|
|
50
|
+
* Returns Git's own numeric exit code (0 on success), never a generic 1 — this command's contract
|
|
51
|
+
* is to propagate Git's exit status literally (see internal/commands/branch.go's
|
|
52
|
+
* defaultGitCheckout, the behavioral reference: it exits the process directly with
|
|
53
|
+
* exitErr.ExitCode() rather than a hardcoded 1). A spawn failure (e.g. the git binary is missing)
|
|
54
|
+
* or a signal-terminated process has no Git-produced numeric code to propagate, so both fall back
|
|
55
|
+
* to 1 — the same convention already used for signal-killed gate commands
|
|
56
|
+
* (docs/cli-parity.md, `trackfw barrier`, "Gate process terminated by a signal ... Recorded as exit 1").
|
|
57
|
+
* @param {string} branchName
|
|
58
|
+
* @returns {number} exit code
|
|
59
|
+
*/
|
|
60
|
+
function defaultGitCheckout(branchName) {
|
|
61
|
+
const result = spawnSync('git', ['checkout', '-b', branchName], { stdio: 'inherit' })
|
|
62
|
+
if (result.error) return 1
|
|
63
|
+
if (result.status === null) return 1
|
|
64
|
+
return result.status
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* runBranchNew implements the `trackfw branch new <type>/<slug>` flow described in
|
|
69
|
+
* docs/req/REQ-2026-08-04-comando-trackfw-branch-new-para-bloquear-criacao-de-branch-sem-req-roadmap-em-wip.md.
|
|
70
|
+
*
|
|
71
|
+
* Mirrors Go's stdout/stderr split exactly (internal/commands/branch.go + root.go Execute()):
|
|
72
|
+
* the primary governance/dry-run message goes to stdout via `writeln`; usage errors and the
|
|
73
|
+
* "blocked: ..." wrapper (Go's `Execute()` printing the returned error) go to stderr via
|
|
74
|
+
* `writeErr`. Git's own output is inherited directly by the child process and is never touched
|
|
75
|
+
* here.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} spec "<type>/<slug>"
|
|
78
|
+
* @param {boolean} dryRun
|
|
79
|
+
* @param {{ loadConfig?: function, resolveWIPDirs?: function, resolveDoneDirs?: function,
|
|
80
|
+
* matchSlug?: function(string, string[], string[]): {matched: boolean, candidates: string[]},
|
|
81
|
+
* execGitCheckout?: function(string): number, writeln?: function(string): void,
|
|
82
|
+
* writeErr?: function(string): void }} deps
|
|
83
|
+
* @returns {number} exit code (0 = success, 1 = blocked/usage error, or Git's own exit code when
|
|
84
|
+
* `git checkout -b` fails — propagated literally, never hardcoded)
|
|
85
|
+
*/
|
|
86
|
+
function runBranchNew(spec, dryRun, deps = {}) {
|
|
87
|
+
const loadConfig = deps.loadConfig || config.load
|
|
88
|
+
const resolveWIPDirs = deps.resolveWIPDirs || validator.resolveWIPDirs
|
|
89
|
+
const resolveDoneDirs = deps.resolveDoneDirs || validator.resolveDoneDirs
|
|
90
|
+
const matchSlug = deps.matchSlug || validator.branchSlugMatchesRoadmap
|
|
91
|
+
const execGitCheckout = deps.execGitCheckout || defaultGitCheckout
|
|
92
|
+
const writeln = deps.writeln || ((s) => process.stdout.write(s + '\n'))
|
|
93
|
+
const writeErr = deps.writeErr || ((s) => process.stderr.write(s + '\n'))
|
|
94
|
+
|
|
95
|
+
let branchType, slug
|
|
96
|
+
try {
|
|
97
|
+
({ branchType, slug } = parseBranchSpec(spec))
|
|
98
|
+
} catch (err) {
|
|
99
|
+
writeErr(err.message)
|
|
100
|
+
return 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const branchName = `${branchType}/${slug}`
|
|
104
|
+
|
|
105
|
+
const cfg = loadConfig()
|
|
106
|
+
const wipDirs = resolveWIPDirs(cfg)
|
|
107
|
+
const doneDirs = resolveDoneDirs(cfg)
|
|
108
|
+
|
|
109
|
+
const normalizedSlug = validator.normalizeBranchSlug(slug)
|
|
110
|
+
const { matched, candidates } = matchSlug(normalizedSlug, wipDirs, doneDirs)
|
|
111
|
+
|
|
112
|
+
if (!matched) {
|
|
113
|
+
const msg = (!candidates || candidates.length === 0)
|
|
114
|
+
? validator.branchGovernanceOrientation(branchName)
|
|
115
|
+
: validator.branchNoMatchingRoadmapMessage(branchName, candidates)
|
|
116
|
+
if (dryRun) {
|
|
117
|
+
writeln(`[dry-run] would block: ${msg}`)
|
|
118
|
+
} else {
|
|
119
|
+
writeln(msg)
|
|
120
|
+
}
|
|
121
|
+
writeErr(`blocked: no matching roadmap in wip/ nor done/ for "${branchName}"`)
|
|
122
|
+
return 1
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (dryRun) {
|
|
126
|
+
writeln(`[dry-run] would create branch "${branchName}" (git checkout -b ${branchName})`)
|
|
127
|
+
return 0
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Git's own stderr (inherited stdio) already reached the user unmodified — do not reformat,
|
|
131
|
+
// duplicate, or replace this exit code with a generic 1.
|
|
132
|
+
return execGitCheckout(branchName)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
branchValidTypes,
|
|
137
|
+
parseBranchSpec,
|
|
138
|
+
defaultGitCheckout,
|
|
139
|
+
runBranchNew,
|
|
140
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// trackfw branch new <type>/<slug>
|
|
4
|
+
//
|
|
5
|
+
// CLI wiring for the `trackfw branch` command group. The testable logic lives in
|
|
6
|
+
// ../branch/runner.js (mirrors the ship.js / ship/runner.js split already used in this CLI).
|
|
7
|
+
|
|
8
|
+
const { Command } = require('commander')
|
|
9
|
+
const { runBranchNew } = require('../branch/runner')
|
|
10
|
+
|
|
11
|
+
function createBranchCommand() {
|
|
12
|
+
const cmd = new Command('branch')
|
|
13
|
+
cmd.description('Manage governed feature branches')
|
|
14
|
+
|
|
15
|
+
const newCmd = new Command('new')
|
|
16
|
+
newCmd
|
|
17
|
+
.description(
|
|
18
|
+
'Create a feat/fix/refactor branch, gated on a matching REQ + roadmap already in wip/ or done/\n\n' +
|
|
19
|
+
'trackfw branch new moves the branch_has_wip_roadmap governance gate (already enforced\n' +
|
|
20
|
+
"by 'trackfw validate' and 'trackfw ship') to before branch creation, instead of after:\n\n" +
|
|
21
|
+
' 1. Validates <type> is one of feat, fix, refactor and <slug> is non-empty.\n' +
|
|
22
|
+
' 2. Checks whether a roadmap in wip/ or done/ matches the given slug — the exact matching\n' +
|
|
23
|
+
" logic 'trackfw validate' already uses (normalized slug, filename contains match).\n" +
|
|
24
|
+
" 3. Without a match: blocks — 'git checkout -b' is never executed — and prints the same\n" +
|
|
25
|
+
" governance orientation message 'trackfw validate' already prints for this rule.\n" +
|
|
26
|
+
" 4. With a match: runs 'git checkout -b <type>/<slug>', propagating Git's own output and\n" +
|
|
27
|
+
' exit status literally.\n\n' +
|
|
28
|
+
'Create the governance artifacts first if this blocks you:\n' +
|
|
29
|
+
' trackfw req new "title"\n' +
|
|
30
|
+
' trackfw roadmap new "title"\n' +
|
|
31
|
+
' trackfw roadmap move <name> wip'
|
|
32
|
+
)
|
|
33
|
+
.argument('<spec>', '<type>/<slug>, type in feat, fix, refactor')
|
|
34
|
+
.option('--dry-run', 'Report whether the branch would be created or blocked, without executing git', false)
|
|
35
|
+
.action((spec, options) => {
|
|
36
|
+
const exitCode = runBranchNew(spec, !!options.dryRun, {})
|
|
37
|
+
if (exitCode !== 0) {
|
|
38
|
+
process.exit(exitCode)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
cmd.addCommand(newCmd)
|
|
43
|
+
return cmd
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = createBranchCommand()
|
package/src/commands/discover.js
CHANGED
|
@@ -435,6 +435,12 @@ cmd.action((opts) => {
|
|
|
435
435
|
} catch (e) {
|
|
436
436
|
console.log(`⚠ agent rules inject partial: ${e.message}`);
|
|
437
437
|
}
|
|
438
|
+
try {
|
|
439
|
+
const { generateAttentionScripts } = require('../generators/hooks');
|
|
440
|
+
generateAttentionScripts({}, cwd);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.warn(`⚠ attention scripts: ${e.message}`);
|
|
443
|
+
}
|
|
438
444
|
try {
|
|
439
445
|
const { injectHooksDetected } = require('../generators/hooks');
|
|
440
446
|
injectHooksDetected(cwd);
|
package/src/commands/index.js
CHANGED
|
@@ -32,6 +32,7 @@ function createProgram() {
|
|
|
32
32
|
program.addCommand(require('./note'))
|
|
33
33
|
program.addCommand(require('./ship'))
|
|
34
34
|
program.addCommand(require('./barrier'))
|
|
35
|
+
program.addCommand(require('./branch'))
|
|
35
36
|
|
|
36
37
|
const { createServeCommand } = require('./serve')
|
|
37
38
|
program.addCommand(createServeCommand())
|
package/src/commands/init.js
CHANGED
|
@@ -8,7 +8,7 @@ const { resolveScope } = require('./integrations')
|
|
|
8
8
|
|
|
9
9
|
const cmd = new Command('init')
|
|
10
10
|
cmd.description(t('init.description'))
|
|
11
|
-
cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,kiro)', '')
|
|
11
|
+
cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,opencode,kiro)', '')
|
|
12
12
|
cmd.option('--identity-preset <preset>', `Agent identity preset: none, neutral, ${identityStore.presetNames().join(', ')}`, 'none')
|
|
13
13
|
cmd.option('--forge <value>', 'Forge platform: github, gitlab, bitbucket, azure', '')
|
|
14
14
|
cmd.action(async (options, command) => {
|
|
@@ -58,7 +58,7 @@ cmd.action(async (options, command) => {
|
|
|
58
58
|
}
|
|
59
59
|
await generators.scaffold(cfg)
|
|
60
60
|
const aiTools = String(options.aiTools || '').split(',').map(tool => tool.trim()).filter(Boolean)
|
|
61
|
-
const supported = new Set(['claude', 'codex', 'gemini', 'antigravity', 'cursor', 'copilot', 'windsurf', 'amazonq', 'kiro'])
|
|
61
|
+
const supported = new Set(['claude', 'codex', 'gemini', 'antigravity', 'cursor', 'copilot', 'windsurf', 'amazonq', 'opencode', 'kiro'])
|
|
62
62
|
// Sem TTY, o escopo nunca é perguntado: default `global` (ADR D1/D4).
|
|
63
63
|
const scope = await resolveScope({}, { interactive: false })
|
|
64
64
|
const makeOnSkip = () => (_destination, reason) => {
|
|
@@ -222,6 +222,7 @@ cmd.action(async (options, command) => {
|
|
|
222
222
|
{ name: 'GitHub Copilot', value: 'copilot' },
|
|
223
223
|
{ name: 'Windsurf', value: 'windsurf' },
|
|
224
224
|
{ name: 'Amazon Q Developer', value: 'amazonq' },
|
|
225
|
+
{ name: 'OpenCode', value: 'opencode' },
|
|
225
226
|
{ name: 'Kiro', value: 'kiro' },
|
|
226
227
|
],
|
|
227
228
|
})
|
package/src/generators/hooks.js
CHANGED
|
@@ -82,8 +82,9 @@ case "$ROADMAP_DIR" in
|
|
|
82
82
|
esac
|
|
83
83
|
|
|
84
84
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
|
|
86
|
+
TOOL_ESC=$(echo "$TOOL" | tr -d '\\000-\\037' | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
|
|
87
|
+
MSG_ESC=$(echo "$MSG" | tr -d '\\000-\\037' | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
|
|
87
88
|
|
|
88
89
|
mkdir -p "$ROADMAP_DIR"
|
|
89
90
|
printf '{"tool":"%s","message":"%s","level":"action_required","timestamp":"%s"}\\n' \\
|
|
@@ -24,7 +24,7 @@ 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
|
|
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.
|
|
28
28
|
|
|
29
29
|
## Barrier protocol
|
|
30
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.
|
|
@@ -35,6 +35,9 @@ Analyze real dependencies between microbatches before assigning work. Microbatch
|
|
|
35
35
|
## Workflow
|
|
36
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.
|
|
37
37
|
|
|
38
|
+
## Dispatch contract
|
|
39
|
+
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.
|
|
40
|
+
|
|
38
41
|
## Post-microbatch audit
|
|
39
42
|
Before releasing the next wave, verify each acceptance criterion yourself: read the changed files, confirm the build, tests and gates, and check that no forbidden file was touched. Green gates are not proof that the intended behavior was delivered — validate the real artifact, not only the test fixtures. A failed audit blocks the next wave.
|
|
40
43
|
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
{"id":"copilot","name":"GitHub Copilot","surfaces":[{"id":"ide","name":"GitHub Copilot","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"custom-agent"},"skills":{"support_level":"native","representation":"agent-skill"}},"paths":{"agents":[{"scope":"global","path":"~/.copilot/agents/trackfw-{{id}}.agent.md","extension":".agent.md"},{"scope":"project","path":".github/agents/trackfw-{{id}}.agent.md","extension":".agent.md"}],"skills":[{"scope":"global","path":"~/.copilot/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".github/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"instructions","surface":"ide","scope":"project","path":".github/copilot-instructions.md","purpose":"repository instructions","required":false},{"id":"hooks","surface":"ide","scope":"project","path":".github/hooks/trackfw-attention.json","purpose":"attention hooks","required":false}]},
|
|
43
43
|
{"id":"windsurf","name":"Windsurf","surfaces":[{"id":"ide","name":"Windsurf IDE","scopes":["global","project"],"capabilities":{"agents":{"support_level":"fallback","representation":"skill","fallback_representation":"specialist skill"},"skills":{"support_level":"native","representation":"skill"}},"paths":{"agents":[{"scope":"global","path":"~/.codeium/windsurf/skills/trackfw-agent-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".windsurf/skills/trackfw-agent-{{id}}/SKILL.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.codeium/windsurf/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".windsurf/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"global-rules","surface":"ide","scope":"project","path":".windsurfrules","purpose":"governance rule discovery","required":false}]},
|
|
44
44
|
{"id":"amazonq","name":"Amazon Q Developer","surfaces":[{"id":"cli","name":"Amazon Q Developer CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"cli-agent-json"},"skills":{"support_level":"fallback","representation":"rule","fallback_representation":"workflow rule"}},"paths":{"agents":[{"scope":"global","path":"~/.aws/amazonq/cli-agents/trackfw-{{id}}.json","extension":".json"},{"scope":"project","path":".amazonq/cli-agents/trackfw-{{id}}.json","extension":".json"}],"skills":[{"scope":"global","path":"~/.aws/amazonq/rules/trackfw-{{id}}.md","extension":".md"},{"scope":"project","path":".amazonq/rules/trackfw-{{id}}.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"guidelines","surface":"cli","scope":"project","path":".amazonq/developer/guidelines.md","purpose":"project governance guidelines","required":false}]},
|
|
45
|
+
{"id":"opencode","name":"OpenCode","surfaces":[{"id":"cli","name":"OpenCode CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"opencode-agent"},"skills":{"support_level":"native","representation":"skill"}},"paths":{"agents":[{"scope":"global","path":"~/.config/opencode/agents/trackfw-{{id}}.md","extension":".md"},{"scope":"project","path":".opencode/agents/trackfw-{{id}}.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.config/opencode/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".opencode/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"instructions","surface":"cli","scope":"project","path":"AGENTS.md","purpose":"repository instructions (shared with Codex/AGENTS.md convention)","required":false}]},
|
|
45
46
|
{"id":"kiro","name":"Kiro","surfaces":[{"id":"ide","name":"Kiro IDE","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"agent-markdown"},"skills":{"support_level":"native","representation":"agent-skill"}},"paths":{"agents":[{"scope":"global","path":"~/.kiro/agents/trackfw-{{id}}.md","extension":".md"},{"scope":"project","path":".kiro/agents/trackfw-{{id}}.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.kiro/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".kiro/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}},{"id":"cli","name":"Kiro CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"agent-json"},"skills":{"support_level":"native","representation":"agent-skill"}},"paths":{"agents":[{"scope":"global","path":"~/.kiro/agents/trackfw-{{id}}.json","extension":".json"},{"scope":"project","path":".kiro/agents/trackfw-{{id}}.json","extension":".json"}],"skills":[{"scope":"global","path":"~/.kiro/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".kiro/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"hooks","surface":"ide","scope":"project","path":".kiro/hooks/trackfw-attention.json","purpose":"attention hooks","required":false}]}
|
|
46
47
|
]
|
|
47
48
|
}
|
|
@@ -278,6 +278,38 @@ function render({ kind, content, capability, item, identity: cfg }) {
|
|
|
278
278
|
return out
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
if (capability.representation === 'opencode-agent') {
|
|
282
|
+
// Reconstrói o frontmatter para o OpenCode CLI (opencode.ai), seguindo o
|
|
283
|
+
// mesmo padrão de reconstrução-do-zero do case "agent-directory".
|
|
284
|
+
// Decisão registrada na Wave 1 do roadmap
|
|
285
|
+
// ROADMAP-2026-08-04-compatibilidade-com-opencode-opencode-ai (achado #3,
|
|
286
|
+
// pesquisa contra o binário real 1.18.13):
|
|
287
|
+
// - "tools:" é uma chave RESERVADA no schema de agente do OpenCode
|
|
288
|
+
// (espera um objeto de overrides por-ferramenta, ex. {bash: false},
|
|
289
|
+
// não uma lista de nomes estilo Claude Code) — reutilizar o
|
|
290
|
+
// frontmatter original faz o OpenCode recusar o carregamento INTEIRO
|
|
291
|
+
// do projeto ("Configuration is invalid"), não só daquele agente. Por
|
|
292
|
+
// isso "tools:" nunca é emitido aqui.
|
|
293
|
+
// - sem "mode:" explícito, o OpenCode assume mode "all" (agente
|
|
294
|
+
// selecionável como persona primária de chat) — os agentes trackfw
|
|
295
|
+
// devem ser sempre subagentes puros, nunca primários, para paridade
|
|
296
|
+
// com o comportamento nos demais targets. Por isso "mode: subagent" é
|
|
297
|
+
// sempre fixo, nunca omitido.
|
|
298
|
+
// - "model:" é deliberadamente OMITIDO (decisão de produto do
|
|
299
|
+
// orquestrador, não uma limitação técnica): o OpenCode espera
|
|
300
|
+
// "provider/model-id" (ex. "anthropic/claude-sonnet-4-5"), não os
|
|
301
|
+
// aliases curtos do catálogo canônico ("opus"/"sonnet"), e mapear
|
|
302
|
+
// para um provider fixo contradiria a motivação de negócio do REQ
|
|
303
|
+
// (permitir que o usuário roteie os agentes trackfw para o modelo
|
|
304
|
+
// open-source/local que ele já configurou em opencode.json). Omitir
|
|
305
|
+
// deixa o OpenCode resolver pelo default já configurado pelo usuário.
|
|
306
|
+
// - "memory:" também não faz sentido no schema do OpenCode e é
|
|
307
|
+
// descartado junto com "tools:".
|
|
308
|
+
let out = `---\ndescription: ${description}\nmode: subagent\n---\n`
|
|
309
|
+
if (body) out += `${body}\n`
|
|
310
|
+
return out
|
|
311
|
+
}
|
|
312
|
+
|
|
281
313
|
if (!id) return normalize(content)
|
|
282
314
|
const withBody = insertBodyPrefix(content, greeting)
|
|
283
315
|
const withFrontmatter = rewriteFrontmatterFields(withBody, name, description)
|
package/src/validator/index.js
CHANGED
|
@@ -1042,6 +1042,45 @@ function validateFilenameUniqueness() {
|
|
|
1042
1042
|
return violations
|
|
1043
1043
|
}
|
|
1044
1044
|
|
|
1045
|
+
// branchSlugMatchesRoadmap verifica se branchSlug (já normalizado via normalizeBranchSlug) casa com o
|
|
1046
|
+
// nome de algum roadmap .md encontrado em wipDirs ou doneDirs. Reutilizada por
|
|
1047
|
+
// validateBranchHasWIPRoadmap e pelo comando `trackfw branch new` — nunca duplicar esta lógica.
|
|
1048
|
+
//
|
|
1049
|
+
// Espelha internal/validator/validator.go:BranchSlugMatchesRoadmap. Retorna { matched, candidates }:
|
|
1050
|
+
// matched indica se algum candidato casou com o slug; candidates lista todos os roadmaps .md
|
|
1051
|
+
// encontrados em wipDirs+doneDirs (para diagnóstico/mensagem de orientação quando matched é false).
|
|
1052
|
+
function branchSlugMatchesRoadmap(branchSlug, wipDirs, doneDirs) {
|
|
1053
|
+
const dirs = [...wipDirs, ...doneDirs]
|
|
1054
|
+
const candidates = []
|
|
1055
|
+
let matched = false
|
|
1056
|
+
for (const dir of dirs) {
|
|
1057
|
+
const files = listDir(dir).filter(f => f.endsWith('.md'))
|
|
1058
|
+
candidates.push(...files)
|
|
1059
|
+
if (files.some(file => normalizeBranchSlug(file).includes(branchSlug))) matched = true
|
|
1060
|
+
}
|
|
1061
|
+
return { matched, candidates }
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// branchGovernanceOrientation is the guidance message printed when a feat/fix/refactor branch has
|
|
1065
|
+
// no roadmap in wip/ nor done/ at all (candidates is empty). Shared by validateBranchHasWIPRoadmap
|
|
1066
|
+
// and `trackfw branch new` — never duplicate this string. Byte-identical to Go's
|
|
1067
|
+
// BranchGovernanceOrientation.
|
|
1068
|
+
function branchGovernanceOrientation(branch) {
|
|
1069
|
+
return `branch "${branch}" is a feat/fix/refactor branch but no roadmap is in wip/ nor done/ — create governance artifacts first:\n trackfw req new "title"\n trackfw roadmap new "title"\n trackfw roadmap move <name> wip`
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// branchNoMatchingRoadmapMessage is the guidance message printed when roadmaps exist in wip/ or
|
|
1073
|
+
// done/ but none of them match the branch's slug. Shared by validateBranchHasWIPRoadmap and
|
|
1074
|
+
// `trackfw branch new` — never duplicate this string. Byte-identical to Go's
|
|
1075
|
+
// BranchNoMatchingRoadmapMessage. Does not mutate candidates.
|
|
1076
|
+
function branchNoMatchingRoadmapMessage(branch, candidates) {
|
|
1077
|
+
// P3: sort for deterministic output regardless of filesystem ordering.
|
|
1078
|
+
const sorted = [...candidates].sort()
|
|
1079
|
+
const display = sorted.slice(0, 3)
|
|
1080
|
+
const suffix = sorted.length > 3 ? `, e mais ${sorted.length - 3}` : ''
|
|
1081
|
+
return `branch "${branch}" has no matching roadmap in wip/ nor done/ (found: ${display.join(', ')}${suffix}) — include the branch slug in the roadmap filename or set TRACKFW_BRANCH explicitly in CI`
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1045
1084
|
// validateBranchHasWIPRoadmap — verifica que branch feat/fix/refactor tem ao menos um roadmap em
|
|
1046
1085
|
// wip/ ou done/ cujo slug case com a branch. Aceita done/ para permitir encerramento do roadmap na
|
|
1047
1086
|
// própria branch, conforme a Definition of Done, sem reprovar o gate.
|
|
@@ -1067,22 +1106,13 @@ function validateBranchHasWIPRoadmap() {
|
|
|
1067
1106
|
const wipDirs = resolveWIPDirs(cfg)
|
|
1068
1107
|
const doneDirs = resolveDoneDirs(cfg)
|
|
1069
1108
|
const branchSlug = normalizeBranchSlug(branch.split('/', 2)[1])
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
for (const dir of [...wipDirs, ...doneDirs]) {
|
|
1073
|
-
const files = listDir(dir).filter(f => f.endsWith('.md'))
|
|
1074
|
-
candidates.push(...files)
|
|
1075
|
-
if (files.some(file => normalizeBranchSlug(file).includes(branchSlug))) return []
|
|
1076
|
-
}
|
|
1109
|
+
const { matched, candidates } = branchSlugMatchesRoadmap(branchSlug, wipDirs, doneDirs)
|
|
1110
|
+
if (matched) return []
|
|
1077
1111
|
|
|
1078
1112
|
if (candidates.length === 0) {
|
|
1079
|
-
return [
|
|
1113
|
+
return [branchGovernanceOrientation(branch)]
|
|
1080
1114
|
}
|
|
1081
|
-
|
|
1082
|
-
const sorted = [...candidates].sort()
|
|
1083
|
-
const display = sorted.slice(0, 3)
|
|
1084
|
-
const suffix = sorted.length > 3 ? `, e mais ${sorted.length - 3}` : ''
|
|
1085
|
-
return [`branch "${branch}" has no matching roadmap in wip/ nor done/ (found: ${display.join(', ')}${suffix}) — include the branch slug in the roadmap filename or set TRACKFW_BRANCH explicitly in CI`]
|
|
1115
|
+
return [branchNoMatchingRoadmapMessage(branch, candidates)]
|
|
1086
1116
|
}
|
|
1087
1117
|
|
|
1088
1118
|
function normalizeBranchSlug(value) {
|
|
@@ -1451,6 +1481,11 @@ module.exports = {
|
|
|
1451
1481
|
validateFolderStatusCoherence,
|
|
1452
1482
|
validateFilenameUniqueness,
|
|
1453
1483
|
validateBranchHasWIPRoadmap,
|
|
1484
|
+
// novas funções — trackfw branch new (extraídas do gate branch_has_wip_roadmap)
|
|
1485
|
+
branchSlugMatchesRoadmap,
|
|
1486
|
+
branchGovernanceOrientation,
|
|
1487
|
+
branchNoMatchingRoadmapMessage,
|
|
1488
|
+
normalizeBranchSlug,
|
|
1454
1489
|
// novas funções ML-2B
|
|
1455
1490
|
contentHasMarker,
|
|
1456
1491
|
ruleSeverity,
|