trackfw 2.12.3 → 2.13.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/README.md +49 -23
- package/package.json +13 -2
- package/src/commands/agents.js +3 -0
- package/src/commands/index.js +2 -0
- package/src/commands/init.js +7 -22
- package/src/commands/integrations.js +67 -0
- package/src/commands/skills.js +3 -0
- package/src/commands/update.js +9 -1
- package/src/generators/codex.js +4 -12
- package/src/generators/init.js +18 -8
- package/src/integrations/assets/agents/architect.md +9 -0
- package/src/integrations/assets/agents/backend.md +9 -0
- package/src/integrations/assets/agents/code-quality.md +9 -0
- package/src/integrations/assets/agents/data.md +9 -0
- package/src/integrations/assets/agents/dba.md +9 -0
- package/src/integrations/assets/agents/frontend.md +9 -0
- package/src/integrations/assets/agents/infra.md +9 -0
- package/src/integrations/assets/agents/qa.md +9 -0
- package/src/integrations/assets/agents/security.md +9 -0
- package/src/integrations/assets/agents/ux.md +9 -0
- package/src/integrations/assets/catalog.json +33 -0
- package/src/integrations/assets/skills/governance.md +6 -0
- package/src/integrations/assets/skills/implement.md +6 -0
- package/src/integrations/assets/skills/plan.md +6 -0
- package/src/integrations/assets/skills/release.md +6 -0
- package/src/integrations/assets/skills/review.md +6 -0
- package/src/integrations/catalog.js +34 -0
- package/src/integrations/index.js +103 -0
- package/src/integrations/legacy.js +35 -0
- package/src/integrations/manager.js +237 -0
- package/src/integrations/render.js +42 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# trackfw
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> AI governance CLI for software delivery teams — ADR → REQ → ROADMAP → backlog / wip / blocked / done / abandoned
|
|
4
4
|
|
|
5
5
|
[](https://github.com/kgsaran/trackfw/releases/latest)
|
|
6
6
|
[](go.mod)
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
[](https://pypi.org/project/trackfw/)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
|
|
11
|
-
**trackfw** is an open-source CLI
|
|
11
|
+
**trackfw** is an open-source governance CLI for AI-native software delivery. It enforces a traceable chain from architectural decision to shipped code — without SaaS, accounts, or databases. Markdown files are state.
|
|
12
|
+
|
|
13
|
+
It is designed for teams looking for an ADR / REQ / ROADMAP governance framework with native support for AI coding assistants such as Codex, Claude Code, Gemini CLI, Antigravity, Cursor, GitHub Copilot, Windsurf, Amazon Q, and Kiro.
|
|
12
14
|
|
|
13
15
|
```
|
|
14
16
|
ADR → REQ → ROADMAP → backlog / wip / blocked / done / abandoned
|
|
@@ -91,8 +93,8 @@ npm install -g trackfw
|
|
|
91
93
|
```
|
|
92
94
|
|
|
93
95
|
The npm package is pure Node.js — no compiled binary or postinstall download.
|
|
94
|
-
It works wherever Node.js ≥ 18 is installed. Shared behavior
|
|
95
|
-
|
|
96
|
+
It works wherever Node.js ≥ 18 is installed. Shared behavior, including the AI
|
|
97
|
+
integration lifecycle, follows the [CLI parity contract](docs/cli-parity.md).
|
|
96
98
|
|
|
97
99
|
### pip
|
|
98
100
|
|
|
@@ -151,15 +153,17 @@ trackfw status
|
|
|
151
153
|
| `trackfw plugins list` | List installed plugins |
|
|
152
154
|
| `trackfw plugins add <user/repo>` | Install a plugin from GitHub Releases |
|
|
153
155
|
| `trackfw plugins remove <name>` | Remove an installed plugin |
|
|
154
|
-
| `trackfw agents` |
|
|
155
|
-
| `trackfw
|
|
156
|
-
| `trackfw
|
|
157
|
-
| `trackfw
|
|
158
|
-
| `trackfw
|
|
159
|
-
| `trackfw
|
|
156
|
+
| `trackfw agents list` | List available agents and deployment state across AI CLIs |
|
|
157
|
+
| `trackfw agents install` | Install selected specialist agents |
|
|
158
|
+
| `trackfw agents update` | Safely update managed agents |
|
|
159
|
+
| `trackfw agents uninstall` | Remove selected owned agent deployments |
|
|
160
|
+
| `trackfw skills list` | List available governance skills and deployment state |
|
|
161
|
+
| `trackfw skills install/update/uninstall` | Manage selected governance skills |
|
|
162
|
+
| `trackfw gemini`, `cursor`, `copilot`, `windsurf`, `amazonq` | Historical aliases in the Go distribution only; use `agents`/`skills` |
|
|
160
163
|
| `trackfw version` | Print version |
|
|
161
164
|
|
|
162
|
-
|
|
165
|
+
The same lifecycle contract is available from the Go/Homebrew, npm, and PyPI
|
|
166
|
+
distributions.
|
|
163
167
|
|
|
164
168
|
---
|
|
165
169
|
|
|
@@ -323,19 +327,41 @@ $ trackfw status
|
|
|
323
327
|
|
|
324
328
|
## AI assistant integration
|
|
325
329
|
|
|
326
|
-
`trackfw init`
|
|
330
|
+
`trackfw init` can install initial AI integrations. The `agents` and `skills`
|
|
331
|
+
command families provide the complete lifecycle in every distribution.
|
|
327
332
|
|
|
328
|
-
|
|
|
329
|
-
|
|
330
|
-
|
|
|
331
|
-
|
|
|
332
|
-
|
|
|
333
|
-
|
|
|
334
|
-
|
|
|
335
|
-
|
|
|
336
|
-
|
|
|
333
|
+
| Target | Native/fallback representation |
|
|
334
|
+
|---|---|
|
|
335
|
+
| Claude Code | Subagent Markdown and Agent Skills |
|
|
336
|
+
| Codex | Custom-agent TOML and Agent Skills |
|
|
337
|
+
| Gemini CLI | Agent Markdown and skills |
|
|
338
|
+
| Antigravity | Agent/skill directories; explicit `legacy-cli` surface available |
|
|
339
|
+
| Cursor | Agent Markdown and skills |
|
|
340
|
+
| GitHub Copilot | Custom agents and Agent Skills |
|
|
341
|
+
| Windsurf | Specialist-skill fallback for agents and native skills |
|
|
342
|
+
| Amazon Q | CLI agent JSON and workflow-rule fallback |
|
|
343
|
+
| Kiro | Native IDE/CLI agents and Agent Skills |
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
# Inspect every deployment, including legacy surfaces
|
|
347
|
+
trackfw agents list --json
|
|
348
|
+
|
|
349
|
+
# Install selected items in the repository
|
|
350
|
+
trackfw agents install --targets codex,claude --items architect,backend --scope project
|
|
351
|
+
trackfw skills install --targets codex,antigravity --items governance,implement --scope project
|
|
352
|
+
|
|
353
|
+
# Select an alternate surface explicitly
|
|
354
|
+
trackfw agents install --targets kiro --surface kiro=cli
|
|
355
|
+
trackfw agents list --targets antigravity --surface antigravity=legacy-cli
|
|
356
|
+
```
|
|
337
357
|
|
|
338
|
-
|
|
358
|
+
Without `--targets`, mutations open a numbered/checkbox selector in a TTY and
|
|
359
|
+
fail with an actionable error in CI. The lifecycle reports `not-installed`,
|
|
360
|
+
`current`, `outdated`, or `modified`. A manifest under `.trackfw/` records
|
|
361
|
+
scope-specific ownership, version, SHA-256, and shared claims. Modified files
|
|
362
|
+
are never replaced or removed unless `--force` is explicit, and unmanaged files
|
|
363
|
+
are never removed. Known historical templates are adopted without overwriting;
|
|
364
|
+
unknown unmanaged content cannot be adopted by `update`, even with `--force`.
|
|
339
365
|
|
|
340
366
|
The 10 roles installed for each tool: **architect · backend · frontend · qa · infra · security · code-quality · dba · ux · data**
|
|
341
367
|
|
|
@@ -350,7 +376,7 @@ The 10 roles installed for each tool: **architect · backend · frontend · qa
|
|
|
350
376
|
? Package manager? npm / pnpm / yarn / bun
|
|
351
377
|
? Git hooks? husky / lefthook / none
|
|
352
378
|
? CI system? GitHub Actions / GitLab CI / none
|
|
353
|
-
? Which AI assistants? Claude
|
|
379
|
+
? Which AI assistants? Claude / Codex / Gemini / Antigravity / Cursor / Copilot / Windsurf / Amazon Q / Kiro
|
|
354
380
|
```
|
|
355
381
|
|
|
356
382
|
The governance structure (`docs/adr/`, `docs/req/`, `docs/roadmaps/`) is always identical — stack-agnostic. The generated hooks, workflows, and AI integrations adapt to your answers.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Governance CLI for AI-native software delivery. Enforces traceability: ADR → REQ → ROADMAP → kanban. Native support for Codex, Claude Code, Gemini CLI, Cursor, Copilot, Windsurf, and Amazon Q.",
|
|
3
|
+
"version": "2.13.0",
|
|
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, and Amazon Q.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"adr",
|
|
@@ -19,11 +19,22 @@
|
|
|
19
19
|
"pre-commit",
|
|
20
20
|
"developer-experience",
|
|
21
21
|
"technical-debt",
|
|
22
|
+
"governance-cli",
|
|
23
|
+
"agents",
|
|
24
|
+
"agents-md",
|
|
25
|
+
"skills",
|
|
26
|
+
"subagents",
|
|
27
|
+
"hooks",
|
|
28
|
+
"ai-coding-assistant",
|
|
22
29
|
"claude-code",
|
|
23
30
|
"openai-codex",
|
|
31
|
+
"gemini-cli",
|
|
24
32
|
"gemini",
|
|
33
|
+
"github-copilot",
|
|
25
34
|
"cursor",
|
|
26
35
|
"copilot",
|
|
36
|
+
"windsurf",
|
|
37
|
+
"amazon-q",
|
|
27
38
|
"req",
|
|
28
39
|
"trackfw"
|
|
29
40
|
],
|
package/src/commands/index.js
CHANGED
|
@@ -27,6 +27,8 @@ function createProgram() {
|
|
|
27
27
|
program.addCommand(require('./help'))
|
|
28
28
|
program.addCommand(require('./configure'))
|
|
29
29
|
program.addCommand(require('./version'))
|
|
30
|
+
program.addCommand(require('./agents'))
|
|
31
|
+
program.addCommand(require('./skills'))
|
|
30
32
|
|
|
31
33
|
const { createServeCommand } = require('./serve')
|
|
32
34
|
program.addCommand(createServeCommand())
|
package/src/commands/init.js
CHANGED
|
@@ -4,7 +4,7 @@ const { t } = require('../i18n')
|
|
|
4
4
|
|
|
5
5
|
const cmd = new Command('init')
|
|
6
6
|
cmd.description(t('init.description'))
|
|
7
|
-
cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (codex,
|
|
7
|
+
cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,kiro)', '')
|
|
8
8
|
cmd.action(async (options) => {
|
|
9
9
|
const path = require('path')
|
|
10
10
|
const generators = require('../generators/init')
|
|
@@ -22,17 +22,10 @@ cmd.action(async (options) => {
|
|
|
22
22
|
}
|
|
23
23
|
await generators.scaffold(cfg)
|
|
24
24
|
const aiTools = String(options.aiTools || '').split(',').map(tool => tool.trim()).filter(Boolean)
|
|
25
|
+
const supported = new Set(['claude', 'codex', 'gemini', 'antigravity', 'cursor', 'copilot', 'windsurf', 'amazonq', 'kiro'])
|
|
25
26
|
for (const tool of aiTools) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
case 'claude': await generators.installAgents(); break
|
|
29
|
-
case 'gemini': await generators.installGemini(); break
|
|
30
|
-
case 'cursor': await generators.installCursor(); break
|
|
31
|
-
case 'copilot': await generators.installCopilot(); break
|
|
32
|
-
case 'windsurf': await generators.installWindsurf(); break
|
|
33
|
-
case 'amazonq': await generators.installAmazonQ(); break
|
|
34
|
-
default: throw new Error(`Unsupported AI tool: ${tool}`)
|
|
35
|
-
}
|
|
27
|
+
if (!supported.has(tool)) throw new Error(`Unsupported AI tool: ${tool}`)
|
|
28
|
+
await generators.installIntegrationTarget(tool, process.cwd())
|
|
36
29
|
}
|
|
37
30
|
console.log(`\n${t('init.success')}`)
|
|
38
31
|
require('../generators/init').printArchitectNextSteps(process.cwd())
|
|
@@ -157,10 +150,12 @@ cmd.action(async (options) => {
|
|
|
157
150
|
{ name: 'Claude Code', value: 'claude' },
|
|
158
151
|
{ name: 'OpenAI Codex', value: 'codex' },
|
|
159
152
|
{ name: 'Gemini CLI', value: 'gemini' },
|
|
153
|
+
{ name: 'Google Antigravity', value: 'antigravity' },
|
|
160
154
|
{ name: 'Cursor', value: 'cursor' },
|
|
161
155
|
{ name: 'GitHub Copilot', value: 'copilot' },
|
|
162
156
|
{ name: 'Windsurf', value: 'windsurf' },
|
|
163
157
|
{ name: 'Amazon Q Developer', value: 'amazonq' },
|
|
158
|
+
{ name: 'Kiro', value: 'kiro' },
|
|
164
159
|
],
|
|
165
160
|
})
|
|
166
161
|
} catch (err) {
|
|
@@ -183,17 +178,7 @@ cmd.action(async (options) => {
|
|
|
183
178
|
const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, requireReqInCommit }
|
|
184
179
|
await generators.scaffold(cfg)
|
|
185
180
|
|
|
186
|
-
for (const tool of (aiTools || []))
|
|
187
|
-
switch (tool) {
|
|
188
|
-
case 'codex': require('../generators/codex').installCodex(process.cwd()); break
|
|
189
|
-
case 'claude': await generators.installAgents(); break
|
|
190
|
-
case 'gemini': await generators.installGemini(); break
|
|
191
|
-
case 'cursor': await generators.installCursor(); break
|
|
192
|
-
case 'copilot': await generators.installCopilot(); break
|
|
193
|
-
case 'windsurf': await generators.installWindsurf(); break
|
|
194
|
-
case 'amazonq': await generators.installAmazonQ(); break
|
|
195
|
-
}
|
|
196
|
-
}
|
|
181
|
+
for (const tool of (aiTools || [])) await generators.installIntegrationTarget(tool, process.cwd())
|
|
197
182
|
|
|
198
183
|
console.log(`\n${t('init.success')}`)
|
|
199
184
|
require('../generators/init').printArchitectNextSteps(process.cwd())
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const { catalog, execute, parseSurfaces } = require('../integrations')
|
|
5
|
+
|
|
6
|
+
const csv = value => String(value).split(',').map(entry => entry.trim()).filter(Boolean)
|
|
7
|
+
const collect = (value, previous) => previous.concat(value)
|
|
8
|
+
|
|
9
|
+
function human(result) {
|
|
10
|
+
const lines = [`Available ${result.kind} (catalog ${result.catalog_version}):`]
|
|
11
|
+
for (const item of result.items) lines.push(` ${item.id.padEnd(14)} ${item.name} — ${item.description}`)
|
|
12
|
+
lines.push('', 'Deployments:')
|
|
13
|
+
for (const deployment of result.deployments) {
|
|
14
|
+
const managed = deployment.managed ? 'managed' : 'unmanaged'
|
|
15
|
+
lines.push(` ${deployment.target.padEnd(12)} ${deployment.surface.padEnd(12)} ${deployment.item.padEnd(14)} ${deployment.state.padEnd(13)} ${deployment.destination} (${managed})`)
|
|
16
|
+
}
|
|
17
|
+
return lines.join('\n')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function promptSelection(kind, options, prompts = require('@inquirer/prompts')) {
|
|
21
|
+
const { checkbox } = prompts
|
|
22
|
+
options.targets = await checkbox({ message: 'Target CLIs', choices: catalog.targets.map(target => ({ name: target.name, value: target.id })), required: true })
|
|
23
|
+
options.items = await checkbox({ message: `${kind} to manage`, choices: catalog[kind].map(item => ({ name: item.name, value: item.id })), required: true })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function promptAmbiguousSurfaces(kind, options, prompts = require('@inquirer/prompts')) {
|
|
27
|
+
const { select } = prompts
|
|
28
|
+
const selected = parseSurfaces(options.surfaces)
|
|
29
|
+
for (const targetID of options.targets || []) {
|
|
30
|
+
if (selected[targetID]) continue
|
|
31
|
+
const target = catalog.targets.find(entry => entry.id === targetID)
|
|
32
|
+
const eligible = target.surfaces.filter(surface => !['legacy', 'unsupported'].includes(surface.capabilities[kind].support_level))
|
|
33
|
+
if (eligible.length <= 1) continue
|
|
34
|
+
const surface = await select({ message: `Surface for ${target.name}`, choices: eligible.map(entry => ({ name: entry.name, value: entry.id })) })
|
|
35
|
+
options.surfaces.push(`${targetID}=${surface}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createLifecycleCommand(kind) {
|
|
40
|
+
const root = new Command(kind).description(`Manage trackfw ${kind}`)
|
|
41
|
+
for (const operation of ['list', 'install', 'uninstall', 'update']) {
|
|
42
|
+
const command = new Command(operation)
|
|
43
|
+
.option('--targets <targets>', 'Comma-separated target CLIs', csv)
|
|
44
|
+
.option('--items <items>', `Comma-separated ${kind} IDs`, csv)
|
|
45
|
+
.option('--scope <scope>', 'Installation scope: project or global', 'project')
|
|
46
|
+
.option('--surface <target=surface>', 'Surface selection (repeatable)', collect, [])
|
|
47
|
+
.option('--json', 'Print deterministic JSON')
|
|
48
|
+
.option('--force', 'Replace or remove modified artifacts')
|
|
49
|
+
command.action(async options => {
|
|
50
|
+
options.surfaces = options.surface || []
|
|
51
|
+
if (options.scope !== 'project' && options.scope !== 'global') throw new Error(`Unsupported scope: ${options.scope}`)
|
|
52
|
+
const mutation = operation !== 'list'
|
|
53
|
+
if (mutation && (!options.targets || !options.targets.length)) {
|
|
54
|
+
if (!process.stdin.isTTY) throw new Error(`${operation} requires --targets in non-interactive mode`)
|
|
55
|
+
await promptSelection(kind, options)
|
|
56
|
+
}
|
|
57
|
+
if (mutation && process.stdin.isTTY) await promptAmbiguousSurfaces(kind, options)
|
|
58
|
+
options.allSurfaces = operation === 'list'
|
|
59
|
+
const output = execute(kind, operation, options)
|
|
60
|
+
console.log(options.json ? JSON.stringify(output) : human(output))
|
|
61
|
+
})
|
|
62
|
+
root.addCommand(command)
|
|
63
|
+
}
|
|
64
|
+
return root
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { createLifecycleCommand, csv, human, promptSelection, promptAmbiguousSurfaces }
|
package/src/commands/update.js
CHANGED
|
@@ -73,7 +73,15 @@ cmd.action(() => {
|
|
|
73
73
|
}
|
|
74
74
|
if (fs.existsSync(path.join(cwd, 'AGENTS.md')) || fs.existsSync(path.join(cwd, '.codex'))) {
|
|
75
75
|
try {
|
|
76
|
-
require('../
|
|
76
|
+
const { buildPlans, IntegrationManager } = require('../integrations');
|
|
77
|
+
const roots = { projectRoot: cwd };
|
|
78
|
+
const manager = new IntegrationManager(roots);
|
|
79
|
+
for (const kind of ['agents', 'skills']) {
|
|
80
|
+
const plans = buildPlans(kind, { targets: ['codex'], scope: 'project' });
|
|
81
|
+
const statuses = manager.inspect(plans);
|
|
82
|
+
const existing = plans.filter((_, index) => statuses[index].state !== 'not-installed');
|
|
83
|
+
manager.update(existing);
|
|
84
|
+
}
|
|
77
85
|
} catch (e) {
|
|
78
86
|
console.warn(` ⚠ Codex integration: ${e.message}`);
|
|
79
87
|
}
|
package/src/generators/codex.js
CHANGED
|
@@ -81,13 +81,11 @@ Review the branch as an owner. Return findings first, ordered by severity, with
|
|
|
81
81
|
`,
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function writeManaged(filePath, content) {
|
|
85
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
86
|
-
fs.writeFileSync(filePath, content.trim() + '\n', 'utf8')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
84
|
function installCodex(cwd) {
|
|
90
85
|
const root = cwd || process.cwd()
|
|
86
|
+
const { execute } = require('../integrations')
|
|
87
|
+
execute('agents', 'install', { targets: ['codex'], scope: 'project' }, { projectRoot: root })
|
|
88
|
+
execute('skills', 'install', { targets: ['codex'], scope: 'project' }, { projectRoot: root })
|
|
91
89
|
injectRulesForTool('codex', root)
|
|
92
90
|
|
|
93
91
|
const configPath = path.join(root, '.codex', 'config.toml')
|
|
@@ -98,14 +96,8 @@ function installCodex(cwd) {
|
|
|
98
96
|
fs.writeFileSync(configPath, config, 'utf8')
|
|
99
97
|
}
|
|
100
98
|
|
|
101
|
-
for (const [name, content] of Object.entries(skills)) {
|
|
102
|
-
writeManaged(path.join(root, '.agents', 'skills', name, 'SKILL.md'), content)
|
|
103
|
-
}
|
|
104
|
-
for (const [name, content] of Object.entries(agents)) {
|
|
105
|
-
writeManaged(path.join(root, '.codex', 'agents', name), content)
|
|
106
|
-
}
|
|
107
99
|
injectCodexHooks(root)
|
|
108
100
|
console.log(' ✓ Codex: AGENTS.md, skills, custom agents and hooks')
|
|
109
101
|
}
|
|
110
102
|
|
|
111
|
-
module.exports = { installCodex }
|
|
103
|
+
module.exports = { installCodex, legacyCodexFixtures: { skills, agents } }
|
package/src/generators/init.js
CHANGED
|
@@ -931,17 +931,13 @@ Oriente o usuário:
|
|
|
931
931
|
// AI tool installers
|
|
932
932
|
// ---------------------------------------------------------------------------
|
|
933
933
|
|
|
934
|
-
/**
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
* ao usuário que esta operação requer o binário Go instalado.
|
|
938
|
-
*/
|
|
939
|
-
async function installAgents() {
|
|
940
|
-
// CLAUDE.md já tratado por generateClaudeMD via injectOrUpdateRules
|
|
941
|
-
console.log(' ✓ claude (agentes — execute `trackfw install agents` com o binário Go para instalar os templates)')
|
|
934
|
+
/** Compatibility entrypoint backed by the canonical integration engine. */
|
|
935
|
+
async function installAgents(cwd = process.cwd()) {
|
|
936
|
+
await installIntegrationTarget('claude', cwd)
|
|
942
937
|
}
|
|
943
938
|
|
|
944
939
|
async function installGemini(cwd = process.cwd()) {
|
|
940
|
+
await installIntegrationTarget('gemini', cwd)
|
|
945
941
|
try {
|
|
946
942
|
injectRulesForTool('gemini', cwd)
|
|
947
943
|
console.log(' ✓ trackfw rules → GEMINI.md')
|
|
@@ -951,6 +947,7 @@ async function installGemini(cwd = process.cwd()) {
|
|
|
951
947
|
}
|
|
952
948
|
|
|
953
949
|
async function installCursor(cwd = process.cwd()) {
|
|
950
|
+
await installIntegrationTarget('cursor', cwd)
|
|
954
951
|
try {
|
|
955
952
|
injectRulesForTool('cursor', cwd)
|
|
956
953
|
console.log(' ✓ trackfw rules → .cursor/rules/trackfw.mdc')
|
|
@@ -960,6 +957,7 @@ async function installCursor(cwd = process.cwd()) {
|
|
|
960
957
|
}
|
|
961
958
|
|
|
962
959
|
async function installCopilot(cwd = process.cwd()) {
|
|
960
|
+
await installIntegrationTarget('copilot', cwd)
|
|
963
961
|
try {
|
|
964
962
|
injectRulesForTool('copilot', cwd)
|
|
965
963
|
console.log(' ✓ trackfw rules → .github/copilot-instructions.md')
|
|
@@ -969,6 +967,7 @@ async function installCopilot(cwd = process.cwd()) {
|
|
|
969
967
|
}
|
|
970
968
|
|
|
971
969
|
async function installWindsurf(cwd = process.cwd()) {
|
|
970
|
+
await installIntegrationTarget('windsurf', cwd)
|
|
972
971
|
try {
|
|
973
972
|
injectRulesForTool('windsurf', cwd)
|
|
974
973
|
console.log(' ✓ trackfw rules → .windsurfrules')
|
|
@@ -978,6 +977,7 @@ async function installWindsurf(cwd = process.cwd()) {
|
|
|
978
977
|
}
|
|
979
978
|
|
|
980
979
|
async function installAmazonQ(cwd = process.cwd()) {
|
|
980
|
+
await installIntegrationTarget('amazonq', cwd)
|
|
981
981
|
try {
|
|
982
982
|
injectRulesForTool('amazonq', cwd)
|
|
983
983
|
console.log(' ✓ trackfw rules → .amazonq/developer/guidelines.md')
|
|
@@ -986,6 +986,15 @@ async function installAmazonQ(cwd = process.cwd()) {
|
|
|
986
986
|
}
|
|
987
987
|
}
|
|
988
988
|
|
|
989
|
+
/** Install the canonical agents and skills for one AI target. */
|
|
990
|
+
async function installIntegrationTarget(target, cwd = process.cwd()) {
|
|
991
|
+
const { execute } = require('../integrations')
|
|
992
|
+
const roots = { projectRoot: cwd }
|
|
993
|
+
execute('agents', 'install', { targets: [target], scope: 'project' }, roots)
|
|
994
|
+
execute('skills', 'install', { targets: [target], scope: 'project' }, roots)
|
|
995
|
+
console.log(` ✓ ${target} agents and skills`)
|
|
996
|
+
}
|
|
997
|
+
|
|
989
998
|
/**
|
|
990
999
|
* generateClaudeCommandsForce — re-gera todos os slash commands, sobrescrevendo arquivos existentes.
|
|
991
1000
|
*/
|
|
@@ -1093,6 +1102,7 @@ module.exports = {
|
|
|
1093
1102
|
installCopilot,
|
|
1094
1103
|
installWindsurf,
|
|
1095
1104
|
installAmazonQ,
|
|
1105
|
+
installIntegrationTarget,
|
|
1096
1106
|
injectRulesForTool,
|
|
1097
1107
|
injectRulesDetected,
|
|
1098
1108
|
trackfwRulesBlock,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-architect
|
|
3
|
+
description: Principal software architect for system design, ADRs and governed multi-agent coordination.
|
|
4
|
+
model: opus
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Architect
|
|
8
|
+
|
|
9
|
+
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.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-backend
|
|
3
|
+
description: Senior backend specialist for APIs, domain logic, integrations and data access.
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Backend
|
|
8
|
+
|
|
9
|
+
Implement only the assigned backend scope. Preserve public contracts, Clean Architecture boundaries, observability and trackfw traceability. Run focused build and tests and report evidence and remaining risks.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-code-quality
|
|
3
|
+
description: Code quality specialist for maintainability, static analysis, refactoring and quality gates.
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Code Quality
|
|
8
|
+
|
|
9
|
+
Assess duplication, complexity, architecture fitness, coverage and code smells. Run the repository's linters and quality gates, then report actionable findings by impact without expanding the assigned scope.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-data
|
|
3
|
+
description: Data engineering and data science specialist for pipelines, quality, analytics and MLOps.
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Data
|
|
8
|
+
|
|
9
|
+
Implement reliable, observable and idempotent data workflows. Validate schemas, lineage, quality expectations and cost before changes, and never substitute invented production data for real contracts.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-dba
|
|
3
|
+
description: Database specialist for modeling, queries, indexes, migrations, backup and recovery.
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# DBA
|
|
8
|
+
|
|
9
|
+
Inspect schemas and execution plans before proposing changes. Favor safe, reversible migrations, validate backup and recovery constraints, and preserve data integrity and least-privilege access.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-frontend
|
|
3
|
+
description: Senior frontend specialist for accessible, localized and performant user interfaces.
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Frontend
|
|
8
|
+
|
|
9
|
+
Implement only the assigned frontend scope. Preserve the design system, WCAG accessibility, localization and browser behavior. Validate the affected build and UI tests before completion.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-infra
|
|
3
|
+
description: Infrastructure specialist for cloud, Kubernetes, GitOps, CI/CD, reliability and FinOps.
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Infrastructure
|
|
8
|
+
|
|
9
|
+
Implement only the assigned infrastructure scope. Prefer declarative, least-privilege and reversible changes. Validate plans and manifests without applying to live environments unless explicitly authorized.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-qa
|
|
3
|
+
description: Quality assurance specialist for unit, integration, contract and end-to-end testing.
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# QA
|
|
8
|
+
|
|
9
|
+
Trace critical flows against roadmap acceptance criteria. Create reproducible tests, investigate regressions and flaky behavior, and report concrete contract gaps with validation evidence.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-security
|
|
3
|
+
description: Security reviewer for trust boundaries, secrets, injection, permissions and supply-chain risk.
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Security
|
|
8
|
+
|
|
9
|
+
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.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-ux
|
|
3
|
+
description: UX and UI specialist for research, interaction design, design systems and accessibility.
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# UX
|
|
8
|
+
|
|
9
|
+
Evaluate user journeys, interaction consistency and WCAG 2.2 AA accessibility. Ground recommendations in evidence and validate complex or uncertain flows visually before production implementation.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.1.0",
|
|
3
|
+
"agents": [
|
|
4
|
+
{"id":"architect","name":"Architect","description":"Architecture, ADRs and governed coordination","asset":"assets/agents/architect.md"},
|
|
5
|
+
{"id":"backend","name":"Backend","description":"Backend APIs, domain logic and integrations","asset":"assets/agents/backend.md"},
|
|
6
|
+
{"id":"frontend","name":"Frontend","description":"Accessible and localized user interfaces","asset":"assets/agents/frontend.md"},
|
|
7
|
+
{"id":"qa","name":"QA","description":"Automated testing and acceptance coverage","asset":"assets/agents/qa.md"},
|
|
8
|
+
{"id":"infra","name":"Infrastructure","description":"Cloud, Kubernetes, delivery and reliability","asset":"assets/agents/infra.md"},
|
|
9
|
+
{"id":"security","name":"Security","description":"Threat analysis and DevSecOps controls","asset":"assets/agents/security.md"},
|
|
10
|
+
{"id":"code-quality","name":"Code Quality","description":"Maintainability and quality gates","asset":"assets/agents/code-quality.md"},
|
|
11
|
+
{"id":"dba","name":"DBA","description":"Database modeling, performance and recovery","asset":"assets/agents/dba.md"},
|
|
12
|
+
{"id":"ux","name":"UX","description":"User experience, design systems and accessibility","asset":"assets/agents/ux.md"},
|
|
13
|
+
{"id":"data","name":"Data","description":"Data pipelines, quality, analytics and MLOps","asset":"assets/agents/data.md"}
|
|
14
|
+
],
|
|
15
|
+
"skills": [
|
|
16
|
+
{"id":"governance","name":"Governance","description":"Inspect and maintain the governance chain","asset":"assets/skills/governance.md"},
|
|
17
|
+
{"id":"plan","name":"Plan","description":"Plan a governed implementation","asset":"assets/skills/plan.md"},
|
|
18
|
+
{"id":"implement","name":"Implement","description":"Execute an approved roadmap","asset":"assets/skills/implement.md"},
|
|
19
|
+
{"id":"review","name":"Review","description":"Review correctness and traceability","asset":"assets/skills/review.md"},
|
|
20
|
+
{"id":"release","name":"Release","description":"Prepare and verify a release","asset":"assets/skills/release.md"}
|
|
21
|
+
],
|
|
22
|
+
"targets": [
|
|
23
|
+
{"id":"claude","name":"Claude Code","surfaces":[{"id":"cli","name":"Claude Code CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"subagent"},"skills":{"support_level":"native","representation":"skill"}},"paths":{"agents":[{"scope":"global","path":"~/.claude/agents/trackfw-{{id}}.md","extension":".md"},{"scope":"project","path":".claude/agents/trackfw-{{id}}.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.claude/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".claude/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"settings","surface":"cli","scope":"project","path":".claude/settings.json","purpose":"attention hooks","required":false}]},
|
|
24
|
+
{"id":"codex","name":"Codex CLI","surfaces":[{"id":"cli","name":"Codex CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"custom-agent-toml"},"skills":{"support_level":"native","representation":"skill"}},"paths":{"agents":[{"scope":"global","path":"~/.codex/agents/trackfw-{{id}}.toml","extension":".toml"},{"scope":"project","path":".codex/agents/trackfw-{{id}}.toml","extension":".toml"}],"skills":[{"scope":"global","path":"~/.agents/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".agents/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"config","surface":"cli","scope":"project","path":".codex/config.toml","purpose":"custom agent configuration","required":false},{"id":"hooks","surface":"cli","scope":"project","path":".codex/hooks.json","purpose":"attention hooks","required":false}]},
|
|
25
|
+
{"id":"gemini","name":"Gemini CLI","surfaces":[{"id":"cli","name":"Gemini CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"agent-markdown"},"skills":{"support_level":"native","representation":"skill"}},"paths":{"agents":[{"scope":"global","path":"~/.gemini/agents/trackfw-{{id}}.md","extension":".md"},{"scope":"project","path":".gemini/agents/trackfw-{{id}}.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.gemini/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".gemini/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"context","surface":"cli","scope":"global","path":"~/.gemini/GEMINI.md","purpose":"global governance context","required":false},{"id":"settings","surface":"cli","scope":"project","path":".gemini/settings.json","purpose":"attention hooks","required":false}]},
|
|
26
|
+
{"id":"antigravity","name":"Google Antigravity","surfaces":[{"id":"current","name":"Antigravity IDE and CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"agent-directory"},"skills":{"support_level":"native","representation":"agent-skill"}},"paths":{"agents":[{"scope":"global","path":"~/.gemini/config/agents/trackfw-{{id}}/agent.md","extension":".md"},{"scope":"project","path":".agents/agents/trackfw-{{id}}/agent.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.gemini/config/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".agents/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}},{"id":"legacy-cli","name":"Antigravity legacy CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"legacy","representation":"agent-json"},"skills":{"support_level":"legacy","representation":"agent-skill"}},"paths":{"agents":[{"scope":"global","path":"~/.gemini/antigravity-cli/agents/trackfw-{{id}}/agent.json","extension":".json"},{"scope":"project","path":".agents/agents/trackfw-{{id}}/agent.json","extension":".json"}],"skills":[{"scope":"global","path":"~/.gemini/antigravity-cli/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".agents/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"rules","surface":"current","scope":"project","path":".agents/rules/trackfw.md","purpose":"workspace governance rules","required":false},{"id":"workflows","surface":"current","scope":"project","path":".agents/workflows/trackfw.md","purpose":"workflow discovery","required":false}]},
|
|
27
|
+
{"id":"cursor","name":"Cursor","surfaces":[{"id":"ide","name":"Cursor IDE","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"agent-markdown"},"skills":{"support_level":"native","representation":"skill"}},"paths":{"agents":[{"scope":"global","path":"~/.cursor/agents/trackfw-{{id}}.md","extension":".md"},{"scope":"project","path":".cursor/agents/trackfw-{{id}}.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.cursor/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".cursor/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"hooks","surface":"ide","scope":"project","path":".cursor/hooks.json","purpose":"attention hooks","required":false}]},
|
|
28
|
+
{"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}]},
|
|
29
|
+
{"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}]},
|
|
30
|
+
{"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}]},
|
|
31
|
+
{"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}]}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-governance
|
|
3
|
+
description: Inspect and maintain ADR, REQ, ROADMAP, traceability, lifecycle state and validation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run `trackfw context --format=json`, read the working context, preserve trace IDs and status-folder coherence, and run `trackfw validate --json` before reporting completion.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-implement
|
|
3
|
+
description: Implement work governed by an existing trackfw requirement and roadmap.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Identify the matching REQ and WIP roadmap before editing. Work one microbatch at a time, run focused project gates plus `trackfw validate --json`, and do not mark completion before acceptance criteria pass.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-plan
|
|
3
|
+
description: Plan governed changes with REQs, ADRs, roadmaps, microbatches and acceptance criteria.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Inspect existing governance artifacts, code and tests. Resolve material decisions in ADRs, create a linked REQ, and produce decision-complete parallelizable microbatches with exact validation commands.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-release
|
|
3
|
+
description: Prepare and verify versioning, changelog, packaging, parity and publication gates.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Confirm traceability and completion, execute quality and package gates, derive SemVer from commits since the latest tag, and stop before publishing unless explicitly authorized.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trackfw-review
|
|
3
|
+
description: Review changes for correctness, security, regression risk, tests and governance traceability.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Compare the branch with its base, verify the linked REQ and roadmap, and report evidence-backed findings by severity. Remain read-only unless fixes are explicitly requested.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
|
|
6
|
+
const ASSET_ROOT = path.join(__dirname, 'assets')
|
|
7
|
+
const catalog = Object.freeze(JSON.parse(fs.readFileSync(path.join(ASSET_ROOT, 'catalog.json'), 'utf8')))
|
|
8
|
+
|
|
9
|
+
function items(kind) {
|
|
10
|
+
if (kind !== 'agents' && kind !== 'skills') throw new Error(`Unsupported integration kind: ${kind}`)
|
|
11
|
+
return catalog[kind]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function target(id) {
|
|
15
|
+
const found = catalog.targets.find(entry => entry.id === id)
|
|
16
|
+
if (!found) throw new Error(`Unsupported target: ${id}`)
|
|
17
|
+
return found
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function surfaceFor(targetEntry, requested, kind = 'agents') {
|
|
21
|
+
const surfaces = targetEntry.surfaces || []
|
|
22
|
+
const found = requested
|
|
23
|
+
? surfaces.find(entry => entry.id === requested)
|
|
24
|
+
: surfaces.find(entry => !['legacy', 'unsupported'].includes(entry.capabilities[kind].support_level)) || surfaces[0]
|
|
25
|
+
if (!found) throw new Error(`Unsupported surface ${requested} for target ${targetEntry.id}`)
|
|
26
|
+
return found
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readAsset(item) {
|
|
30
|
+
const relative = item.asset.replace(/^assets\//, '')
|
|
31
|
+
return fs.readFileSync(path.join(ASSET_ROOT, relative), 'utf8')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { catalog, items, target, surfaceFor, readAsset }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { catalog, items, target, surfaceFor, readAsset } = require('./catalog')
|
|
4
|
+
const { render } = require('./render')
|
|
5
|
+
const { IntegrationManager } = require('./manager')
|
|
6
|
+
const { legacyHashes } = require('./legacy')
|
|
7
|
+
|
|
8
|
+
function parseSurfaces(values = []) {
|
|
9
|
+
const result = {}
|
|
10
|
+
for (const value of values) {
|
|
11
|
+
const [targetID, surfaceID, extra] = String(value).split('=')
|
|
12
|
+
if (!targetID || !surfaceID || extra !== undefined) throw new Error(`Invalid --surface ${value}: expected target=surface`)
|
|
13
|
+
if (result[targetID]) throw new Error(`Duplicate --surface for target ${targetID}`)
|
|
14
|
+
result[targetID] = surfaceID
|
|
15
|
+
}
|
|
16
|
+
return result
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function selections(kind, options = {}) {
|
|
20
|
+
const selectedItems = options.items && options.items.length ? options.items : items(kind).map(item => item.id)
|
|
21
|
+
const itemEntries = selectedItems.map(id => {
|
|
22
|
+
const found = items(kind).find(item => item.id === id)
|
|
23
|
+
if (!found) throw new Error(`Unsupported ${kind} item: ${id}`)
|
|
24
|
+
return found
|
|
25
|
+
})
|
|
26
|
+
const targetValues = options.targets && options.targets.length ? options.targets : catalog.targets.map(entry => entry.id)
|
|
27
|
+
const surfaceSelections = parseSurfaces(options.surfaces)
|
|
28
|
+
const scopes = options.scope ? [options.scope] : ['project']
|
|
29
|
+
const targets = []
|
|
30
|
+
for (const targetID of targetValues) {
|
|
31
|
+
const targetEntry = target(targetID)
|
|
32
|
+
const selected = surfaceSelections[targetID]
|
|
33
|
+
const surfaces = options.allSurfaces && !selected
|
|
34
|
+
? targetEntry.surfaces.filter(entry => entry.capabilities[kind].support_level !== 'unsupported')
|
|
35
|
+
: [surfaceFor(targetEntry, selected, kind)]
|
|
36
|
+
for (const surface of surfaces) targets.push({ target: targetEntry, surface })
|
|
37
|
+
}
|
|
38
|
+
return { itemEntries, targets, scopes }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildPlans(kind, options = {}) {
|
|
42
|
+
const selected = selections(kind, options)
|
|
43
|
+
const plans = []
|
|
44
|
+
for (const { target: targetEntry, surface } of selected.targets) {
|
|
45
|
+
const capability = surface.capabilities[kind]
|
|
46
|
+
if (capability.support_level === 'unsupported') continue
|
|
47
|
+
for (const scope of selected.scopes) {
|
|
48
|
+
if (!surface.scopes.includes(scope)) continue
|
|
49
|
+
const paths = surface.paths[kind].filter(entry => entry.scope === scope)
|
|
50
|
+
for (const item of selected.itemEntries) {
|
|
51
|
+
for (const installPath of paths) {
|
|
52
|
+
const destination = installPath.path.replace('{{id}}', item.id)
|
|
53
|
+
const content = render({ target: targetEntry.id, kind, item, content: readAsset(item), capability, destination })
|
|
54
|
+
const claim = { target: targetEntry.id, surface: surface.id, scope, kind, item: item.id }
|
|
55
|
+
plans.push({
|
|
56
|
+
claim,
|
|
57
|
+
destination,
|
|
58
|
+
content,
|
|
59
|
+
catalogVersion: catalog.version,
|
|
60
|
+
supportLevel: capability.support_level,
|
|
61
|
+
representation: capability.representation,
|
|
62
|
+
legacyHashes: legacyHashes(claim),
|
|
63
|
+
item,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return plans.sort((a, b) => [a.claim.target, a.claim.surface, a.claim.scope, a.claim.item, a.destination].join('\0').localeCompare([b.claim.target, b.claim.surface, b.claim.scope, b.claim.item, b.destination].join('\0')))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function result(kind, plans, statuses) {
|
|
73
|
+
return {
|
|
74
|
+
kind,
|
|
75
|
+
catalog_version: catalog.version,
|
|
76
|
+
items: items(kind).map(({ id, name, description }) => ({ id, name, description })),
|
|
77
|
+
deployments: statuses.map((status, index) => ({
|
|
78
|
+
target: status.claim.target,
|
|
79
|
+
surface: status.claim.surface,
|
|
80
|
+
scope: status.claim.scope,
|
|
81
|
+
item: status.claim.item,
|
|
82
|
+
support_level: status.supportLevel,
|
|
83
|
+
representation: status.representation,
|
|
84
|
+
destination: plans[index].destination,
|
|
85
|
+
state: status.state,
|
|
86
|
+
managed: status.managed,
|
|
87
|
+
})),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function execute(kind, operation, options = {}, roots = {}) {
|
|
92
|
+
const plans = buildPlans(kind, options)
|
|
93
|
+
const manager = new IntegrationManager(roots)
|
|
94
|
+
let statuses
|
|
95
|
+
if (operation === 'list') statuses = manager.inspect(plans)
|
|
96
|
+
else if (operation === 'install') statuses = manager.install(plans, { force: options.force })
|
|
97
|
+
else if (operation === 'update') statuses = manager.update(plans, { force: options.force })
|
|
98
|
+
else if (operation === 'uninstall') statuses = manager.uninstall(plans, { force: options.force })
|
|
99
|
+
else throw new Error(`Unsupported integration operation: ${operation}`)
|
|
100
|
+
return result(kind, plans, statuses)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { catalog, buildPlans, execute, IntegrationManager, parseSurfaces }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// SHA-256 values emitted by the pre-lifecycle installers. They are deliberately
|
|
4
|
+
// static: only byte-identical historical templates may be adopted.
|
|
5
|
+
const LEGACY_HASHES = Object.freeze({
|
|
6
|
+
// Go/Homebrew `trackfw agents` Claude templates.
|
|
7
|
+
'claude:cli:global:agents:architect': ['d28ae507d2ce9fd3fcd7cb1a0c1ffaaebc994dc9c45b219e5361b909dc6132ba'],
|
|
8
|
+
'claude:cli:global:agents:backend': ['587bf790907bc7451976c026b9c7dc5419541a5fdfb064586744198dcf8c0439'],
|
|
9
|
+
'claude:cli:global:agents:code-quality': ['8917909b55ed54cfa8bb66015a301428f4d924bb3658a9c4c5d443e14e60d399'],
|
|
10
|
+
'claude:cli:global:agents:data': ['3a402a85610cf3d463200d0f81fda61b5c6c02a0198d302a5d6da49e8e5ff688'],
|
|
11
|
+
'claude:cli:global:agents:dba': ['07aa92c09833a7c72c3d9b3e39ec1272a91e435deda2da5ddef321abed3702fa'],
|
|
12
|
+
'claude:cli:global:agents:frontend': ['7ae3940a0827b0047363cd984be5e96b66902098c5ea669d0975a234027ca39c'],
|
|
13
|
+
'claude:cli:global:agents:infra': ['9f5534c83c3d3b0a9e8c550bd7a3cbb861eb7c4d7671fe8783cd68ff5d5c1605'],
|
|
14
|
+
'claude:cli:global:agents:qa': ['384283eb46d5e3c5ee978fc87e1b5cd44009e3c62d57c8dcd5cf92189b79e291'],
|
|
15
|
+
'claude:cli:global:agents:security': ['10e02aa03fe502174174e376d9ec845c549f828af885059214af23854efb4c3f'],
|
|
16
|
+
'claude:cli:global:agents:ux': ['49b5bbcb8063075bda0b254d1040ae7d2b8d8c315f538cf05e39774ab8a2907b'],
|
|
17
|
+
|
|
18
|
+
// npm `installCodex`: five catalog agents and five workflow skills.
|
|
19
|
+
'codex:cli:project:agents:architect': ['c7e4b34984a8ba54753ecb9c0b2a1ad10b0d2083a15e03bcb19016012efa39e1', '38199d6edfc0fd7c5d6663996541a25a17203d87713709d0a0daad2ecc5d6be6', '94512bd3db605d5170841018adc2246ce314a57b3be07938fc61d0c41ef6126c'],
|
|
20
|
+
'codex:cli:project:agents:backend': ['0e2327ace2c719fcb0abe2f17dfdbad4a0d987824f888e6802a9f4a132163f64', '348c5ab0a57597f332c2a5b293f045a0d3980875a9c9083a06cad15f909e23a7', '43759e9a912bb236a997d58ad184dfcda2b45f6e00ed5a5edad93d83dd9c906d'],
|
|
21
|
+
'codex:cli:project:agents:frontend': ['1a377e6c9a51f1df549e475558897dac428f94ae50e6955a1ca99cb0b04b0648', '608c10cc0f910725ad813736a2548e1c66479abce28d32d4b8969707be1054f1', '5ebf3a404d300e17338538eeb8f50d63bda8fde5f5f0bf0b73767434233ae9c4'],
|
|
22
|
+
'codex:cli:project:agents:qa': ['aad7e86d59e511e6b1519e2e26bb4de376eb124725a44d82f704edf396989ac0', '3e8b3a5d04aa44c62ce6af801be9d907aa6458931c19676ca999776ac961254d', '33663bb0ee4bee4cd971a691283649b308ad093a01000762a70ec79481d10e52'],
|
|
23
|
+
'codex:cli:project:agents:security': ['b7743d40163066794941f37fab259f965bb78c54c9a8fe3cc7a0146d2552f016', 'cdd197001ebabf8cf63b8c1562d0eb89b226df8a2e5dcb1ce84e9a1ea6de782d', '1e3987835233bbdb0751e98a50ec7512dfc7855e293aa40d1ad77c80dde1945f'],
|
|
24
|
+
'codex:cli:project:skills:governance': ['54edf417d53e2a91f52651de5a31a9c1b80be8d0f7f1252c5b9f3b55630a9b96', 'c30786495105715ae707d374b414d21344b20a0cd354713f8173c5109d690fd9', 'be044d1a1f2af4835d476a3f4798b35e2067eeebdff8f30872353eee6b212736'],
|
|
25
|
+
'codex:cli:project:skills:implement': ['bb9bbe197244cd4537610579b324757e8c7f97755a78fdb01daab4638485bfc2', '898682d21e6ba938a2ef2fe4908f4047358e3f27ca291068a5585a47aca1426e', 'c5a53c5e9444810d3beeebe563798de792615c7703a0d99e954d578d068af4d4'],
|
|
26
|
+
'codex:cli:project:skills:plan': ['dc038a9f18bd0fee7e03d8c7816f058e9c95a4e729dddfb086d642a61bc676ba', '2ef36f3ac8c77eb46d13ba4cbe420e1c872f71edace9b7851d63cb50bc5f3708', 'b2cd10338db33369e78656a1e3877427a454f9eab6fa1ec948fd17730c66b46a'],
|
|
27
|
+
'codex:cli:project:skills:release': ['2db9c460046da9cb918acb7f716727a07fdb5b361f2a81a67c35b8fe09135fcd', '2450fd34b81a14d28c794abc0e9ac0213ba929a950d15b3226443c92419d42c9'],
|
|
28
|
+
'codex:cli:project:skills:review': ['bea0debcb3b8f5014a7c7ae0e93b9b8c4b318e14c8fc8ff6510594410a123783', 'b17fbc6d59bdfad64d2c1993cc588c0e6e9ae8e4b4a16669abf254d41cc40330', '8b3119641a0d06b99b451cf6437c15a2f7f58069fb0ebf855ab757f66b919b53'],
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function legacyHashes(claim) {
|
|
32
|
+
return [...(LEGACY_HASHES[`${claim.target}:${claim.surface}:${claim.scope}:${claim.kind}:${claim.item}`] || [])]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { LEGACY_HASHES, legacyHashes }
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const os = require('node:os')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
|
|
8
|
+
const SCHEMA_VERSION = 1
|
|
9
|
+
const sha256 = content => crypto.createHash('sha256').update(content).digest('hex')
|
|
10
|
+
const claimKey = claim => [claim.target, claim.surface, claim.scope, claim.kind, claim.item].join('\u0000')
|
|
11
|
+
const cleanClaim = claim => ({ target: claim.target, surface: claim.surface, scope: claim.scope, kind: claim.kind, item: claim.item })
|
|
12
|
+
|
|
13
|
+
class IntegrationManager {
|
|
14
|
+
constructor({ projectRoot = process.cwd(), homeRoot = os.homedir() } = {}) {
|
|
15
|
+
this.roots = { project: path.resolve(projectRoot), global: path.resolve(homeRoot) }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
manifestPath(scope) { return path.join(this.roots[scope], '.trackfw', 'integrations-manifest.json') }
|
|
19
|
+
|
|
20
|
+
resolve(scope, destination) {
|
|
21
|
+
const root = this.roots[scope]
|
|
22
|
+
if (!root) throw new Error(`Unsupported scope: ${scope}`)
|
|
23
|
+
if (typeof destination !== 'string' || destination.includes('\u0000') || destination.includes('\\')) throw new Error(`Unsafe destination: ${destination}`)
|
|
24
|
+
let resolved
|
|
25
|
+
if (destination.startsWith('~/')) {
|
|
26
|
+
if (scope !== 'global') throw new Error('Home destination requires global scope')
|
|
27
|
+
resolved = path.resolve(root, destination.slice(2))
|
|
28
|
+
} else if (path.isAbsolute(destination)) {
|
|
29
|
+
resolved = path.normalize(destination)
|
|
30
|
+
} else {
|
|
31
|
+
if (!destination || path.normalize(destination) !== destination || destination === '.' || destination.startsWith(`..${path.sep}`)) throw new Error(`Unsafe destination: ${destination}`)
|
|
32
|
+
resolved = path.resolve(root, destination)
|
|
33
|
+
}
|
|
34
|
+
const rel = path.relative(root, resolved)
|
|
35
|
+
if (!rel || rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) throw new Error(`Destination is outside ${scope} root: ${destination}`)
|
|
36
|
+
this.assertNoSymlinks(root, resolved)
|
|
37
|
+
this.assertNoSymlinks(root, this.manifestPath(scope))
|
|
38
|
+
return resolved
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
assertNoSymlinks(root, destination) {
|
|
42
|
+
let current = destination
|
|
43
|
+
while (true) {
|
|
44
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) throw new Error(`Symlink path is not allowed: ${current}`)
|
|
45
|
+
if (current === root) return
|
|
46
|
+
const parent = path.dirname(current)
|
|
47
|
+
const rel = path.relative(root, current)
|
|
48
|
+
if (parent === current || rel === '..' || rel.startsWith(`..${path.sep}`)) throw new Error(`Path escapes root: ${destination}`)
|
|
49
|
+
current = parent
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
loadManifest(scope) {
|
|
54
|
+
const file = this.manifestPath(scope)
|
|
55
|
+
this.assertNoSymlinks(this.roots[scope], file)
|
|
56
|
+
if (!fs.existsSync(file)) return { schema_version: SCHEMA_VERSION, artifacts: {} }
|
|
57
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
58
|
+
if (parsed.schema_version !== SCHEMA_VERSION || !parsed.artifacts || Array.isArray(parsed.artifacts)) throw new Error(`Unsupported integration manifest: ${file}`)
|
|
59
|
+
return parsed
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
atomicWrite(file, content, mode) {
|
|
63
|
+
const root = this.rootFor(file)
|
|
64
|
+
this.assertNoSymlinks(root, file)
|
|
65
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
66
|
+
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`)
|
|
67
|
+
try {
|
|
68
|
+
fs.writeFileSync(tmp, content, { mode })
|
|
69
|
+
fs.chmodSync(tmp, mode)
|
|
70
|
+
fs.renameSync(tmp, file)
|
|
71
|
+
fs.chmodSync(file, mode)
|
|
72
|
+
} finally {
|
|
73
|
+
if (fs.existsSync(tmp)) fs.unlinkSync(tmp)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
rootFor(file) {
|
|
78
|
+
const found = Object.values(this.roots).find(root => {
|
|
79
|
+
const rel = path.relative(root, file)
|
|
80
|
+
return rel && rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel)
|
|
81
|
+
})
|
|
82
|
+
if (!found) throw new Error(`Path is outside integration roots: ${file}`)
|
|
83
|
+
return found
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
saveManifest(scope, manifest) {
|
|
87
|
+
const artifacts = {}
|
|
88
|
+
for (const destination of Object.keys(manifest.artifacts).sort()) {
|
|
89
|
+
const artifact = manifest.artifacts[destination]
|
|
90
|
+
artifact.claims = artifact.claims.map(cleanClaim).sort((a, b) => claimKey(a).localeCompare(claimKey(b)))
|
|
91
|
+
artifacts[destination] = artifact
|
|
92
|
+
}
|
|
93
|
+
this.atomicWrite(this.manifestPath(scope), `${JSON.stringify({ schema_version: SCHEMA_VERSION, artifacts }, null, 2)}\n`, 0o600)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
inspect(plans) {
|
|
97
|
+
const manifests = new Map()
|
|
98
|
+
return plans.map(plan => {
|
|
99
|
+
const scope = plan.claim.scope
|
|
100
|
+
if (!manifests.has(scope)) manifests.set(scope, this.loadManifest(scope))
|
|
101
|
+
const file = this.resolve(scope, plan.destination)
|
|
102
|
+
const record = manifests.get(scope).artifacts[file]
|
|
103
|
+
const managed = Boolean(record && record.claims.some(claim => claimKey(claim) === claimKey(plan.claim)))
|
|
104
|
+
if (!fs.existsSync(file)) return { ...plan, destination: file, state: 'not-installed', managed }
|
|
105
|
+
const actual = sha256(fs.readFileSync(file))
|
|
106
|
+
const desired = sha256(plan.content)
|
|
107
|
+
let state
|
|
108
|
+
if (record) {
|
|
109
|
+
if (actual !== record.sha256) state = 'modified'
|
|
110
|
+
else if (actual !== desired || record.catalog_version !== plan.catalogVersion) state = 'outdated'
|
|
111
|
+
else state = 'current'
|
|
112
|
+
} else if (actual === desired) state = 'current'
|
|
113
|
+
else if ((plan.legacyHashes || []).includes(actual)) state = 'outdated'
|
|
114
|
+
else state = 'modified'
|
|
115
|
+
return { ...plan, destination: file, state, managed }
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
install(plans, { force = false } = {}) { return this.mutate('install', plans, force) }
|
|
120
|
+
update(plans, { force = false } = {}) { return this.mutate('update', plans, force) }
|
|
121
|
+
uninstall(plans, { force = false } = {}) { return this.mutate('uninstall', plans, force) }
|
|
122
|
+
|
|
123
|
+
mutate(operation, plans, force) {
|
|
124
|
+
const resolved = plans.map(plan => ({ plan, file: this.resolve(plan.claim.scope, plan.destination) }))
|
|
125
|
+
const manifests = new Map()
|
|
126
|
+
for (const { plan } of resolved) if (!manifests.has(plan.claim.scope)) manifests.set(plan.claim.scope, this.loadManifest(plan.claim.scope))
|
|
127
|
+
const desiredByFile = new Map()
|
|
128
|
+
for (const item of resolved) {
|
|
129
|
+
const desired = sha256(item.plan.content)
|
|
130
|
+
if (operation !== 'uninstall' && desiredByFile.has(item.file) && desiredByFile.get(item.file) !== desired) throw new Error(`Conflicting content planned for: ${item.file}`)
|
|
131
|
+
desiredByFile.set(item.file, desired)
|
|
132
|
+
this.preflight(operation, item, manifests.get(item.plan.claim.scope), force)
|
|
133
|
+
}
|
|
134
|
+
const snapshots = new Map()
|
|
135
|
+
for (const item of resolved) this.snapshot(snapshots, item.file)
|
|
136
|
+
for (const scope of manifests.keys()) this.snapshot(snapshots, this.manifestPath(scope))
|
|
137
|
+
try {
|
|
138
|
+
for (const item of resolved) this.apply(operation, item, manifests.get(item.plan.claim.scope), force)
|
|
139
|
+
for (const [scope, manifest] of [...manifests].sort(([a], [b]) => a.localeCompare(b))) this.saveManifest(scope, manifest)
|
|
140
|
+
} catch (error) {
|
|
141
|
+
this.rollback(snapshots)
|
|
142
|
+
throw error
|
|
143
|
+
}
|
|
144
|
+
return this.inspect(plans)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
preflight(operation, { plan, file }, manifest, force) {
|
|
148
|
+
const status = this.inspectResolved(plan, file, manifest)
|
|
149
|
+
const record = manifest.artifacts[file]
|
|
150
|
+
const owned = Boolean(record && record.claims.some(claim => claimKey(claim) === claimKey(plan.claim)))
|
|
151
|
+
if (operation === 'install') {
|
|
152
|
+
if (status.state === 'modified' && !force) throw new Error(`Artifact is modified; use --force: ${file}`)
|
|
153
|
+
if (status.state === 'outdated' && owned && !force) throw new Error(`Artifact is outdated; use update: ${file}`)
|
|
154
|
+
} else if (operation === 'update') {
|
|
155
|
+
if (!owned && status.state === 'modified') throw new Error(`Unmanaged artifact does not match a trackfw template: ${file}`)
|
|
156
|
+
if (status.state === 'modified' && !force) throw new Error(`Artifact is modified; use --force: ${file}`)
|
|
157
|
+
} else if (operation === 'uninstall' && owned && status.state === 'modified' && !force) {
|
|
158
|
+
throw new Error(`Artifact is modified; use --force: ${file}`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
inspectResolved(plan, file, manifest) {
|
|
163
|
+
const record = manifest.artifacts[file]
|
|
164
|
+
const managed = Boolean(record && record.claims.some(claim => claimKey(claim) === claimKey(plan.claim)))
|
|
165
|
+
if (!fs.existsSync(file)) return { state: 'not-installed', managed }
|
|
166
|
+
const actual = sha256(fs.readFileSync(file))
|
|
167
|
+
const desired = sha256(plan.content)
|
|
168
|
+
if (record) {
|
|
169
|
+
if (actual !== record.sha256) return { state: 'modified', managed }
|
|
170
|
+
return { state: actual === desired && record.catalog_version === plan.catalogVersion ? 'current' : 'outdated', managed }
|
|
171
|
+
}
|
|
172
|
+
if (actual === desired) return { state: 'current', managed: false }
|
|
173
|
+
if ((plan.legacyHashes || []).includes(actual)) return { state: 'outdated', managed: false }
|
|
174
|
+
return { state: 'modified', managed: false }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
apply(operation, { plan, file }, manifest, force) {
|
|
178
|
+
let record = manifest.artifacts[file]
|
|
179
|
+
const key = claimKey(plan.claim)
|
|
180
|
+
const owned = Boolean(record && record.claims.some(claim => claimKey(claim) === key))
|
|
181
|
+
if (operation === 'uninstall') {
|
|
182
|
+
if (!owned) return
|
|
183
|
+
record.claims = record.claims.filter(claim => claimKey(claim) !== key)
|
|
184
|
+
if (record.claims.length) return
|
|
185
|
+
if (fs.existsSync(file)) fs.unlinkSync(file)
|
|
186
|
+
delete manifest.artifacts[file]
|
|
187
|
+
this.cleanEmpty(path.dirname(file), this.roots[plan.claim.scope])
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const exists = fs.existsSync(file)
|
|
192
|
+
let actual = exists ? sha256(fs.readFileSync(file)) : ''
|
|
193
|
+
const desired = sha256(plan.content)
|
|
194
|
+
const knownLegacy = (plan.legacyHashes || []).includes(actual)
|
|
195
|
+
let writeDesired = !exists
|
|
196
|
+
if (exists && !owned) writeDesired = (operation === 'update' && actual !== desired) || (force && actual !== desired)
|
|
197
|
+
else if (exists && owned) writeDesired = actual !== desired
|
|
198
|
+
if (!record) record = { destination: file, sha256: '', catalog_version: '', claims: [] }
|
|
199
|
+
if (writeDesired) {
|
|
200
|
+
this.atomicWrite(file, plan.content, 0o644)
|
|
201
|
+
actual = desired
|
|
202
|
+
} else if (exists && !owned && actual !== desired && !knownLegacy && !force) {
|
|
203
|
+
throw new Error(`Unmanaged artifact does not match a trackfw template: ${file}`)
|
|
204
|
+
}
|
|
205
|
+
if (!record.claims.some(claim => claimKey(claim) === key)) record.claims.push(cleanClaim(plan.claim))
|
|
206
|
+
record.sha256 = actual
|
|
207
|
+
record.catalog_version = actual === desired ? plan.catalogVersion : 'legacy'
|
|
208
|
+
manifest.artifacts[file] = record
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
snapshot(snapshots, file) {
|
|
212
|
+
if (snapshots.has(file)) return
|
|
213
|
+
if (!fs.existsSync(file)) snapshots.set(file, null)
|
|
214
|
+
else snapshots.set(file, { content: fs.readFileSync(file), mode: fs.statSync(file).mode & 0o777 })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
rollback(snapshots) {
|
|
218
|
+
for (const [file, snapshot] of [...snapshots].reverse()) {
|
|
219
|
+
try {
|
|
220
|
+
if (!snapshot) { if (fs.existsSync(file) && !fs.lstatSync(file).isDirectory()) fs.unlinkSync(file) }
|
|
221
|
+
else this.atomicWrite(file, snapshot.content, snapshot.mode)
|
|
222
|
+
} catch { /* preserve original error */ }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
cleanEmpty(directory, root) {
|
|
227
|
+
while (directory !== root) {
|
|
228
|
+
const rel = path.relative(root, directory)
|
|
229
|
+
if (!rel || rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) return
|
|
230
|
+
if (!fs.existsSync(directory) || fs.lstatSync(directory).isSymbolicLink() || fs.readdirSync(directory).length) return
|
|
231
|
+
fs.rmdirSync(directory)
|
|
232
|
+
directory = path.dirname(directory)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { IntegrationManager, sha256, claimKey, SCHEMA_VERSION }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function normalize(content) {
|
|
4
|
+
return `${content.trim()}\n`
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function markdownParts(content) {
|
|
8
|
+
const text = content.trim()
|
|
9
|
+
let name = 'trackfw-agent'
|
|
10
|
+
let description = 'trackfw specialist'
|
|
11
|
+
let body = text
|
|
12
|
+
if (text.startsWith('---\n')) {
|
|
13
|
+
const end = text.indexOf('\n---', 4)
|
|
14
|
+
if (end >= 0) {
|
|
15
|
+
const frontmatter = text.slice(4, end)
|
|
16
|
+
body = text.slice(end + 4).trim()
|
|
17
|
+
for (const line of frontmatter.split('\n')) {
|
|
18
|
+
const separator = line.indexOf(':')
|
|
19
|
+
if (separator < 0) continue
|
|
20
|
+
const key = line.slice(0, separator).trim()
|
|
21
|
+
const value = line.slice(separator + 1).trim().replace(/^['"]|['"]$/g, '')
|
|
22
|
+
if (key === 'name') name = value
|
|
23
|
+
if (key === 'description') description = value
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { name, description, body }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function render({ kind, content, capability }) {
|
|
31
|
+
if (kind === 'skills') return normalize(content)
|
|
32
|
+
const parts = markdownParts(content)
|
|
33
|
+
if (capability.representation === 'custom-agent-toml') {
|
|
34
|
+
return `name = ${JSON.stringify(parts.name.replaceAll('-', '_'))}\ndescription = ${JSON.stringify(parts.description)}\ndeveloper_instructions = ${JSON.stringify(parts.body)}\n`
|
|
35
|
+
}
|
|
36
|
+
if (capability.representation === 'cli-agent-json' || capability.representation === 'agent-json') {
|
|
37
|
+
return `${JSON.stringify({ name: parts.name, description: parts.description, prompt: parts.body }, null, 2)}\n`
|
|
38
|
+
}
|
|
39
|
+
return normalize(content)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { render, markdownParts }
|