trackfw 3.0.0 → 4.0.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 +94 -3
- package/package.json +1 -1
- package/src/commands/barrier.js +358 -0
- package/src/commands/discover.js +18 -0
- package/src/commands/help.js +99 -9
- package/src/commands/index.js +3 -0
- package/src/commands/init.js +40 -2
- package/src/commands/log.js +2 -1
- package/src/commands/note.js +24 -0
- package/src/commands/req.js +7 -1
- package/src/commands/roadmap.js +4 -3
- package/src/commands/ship.js +49 -0
- package/src/commands/update-harness.js +208 -0
- package/src/commands/update.js +206 -72
- package/src/config/index.js +5 -2
- package/src/forge/adapter.js +189 -0
- package/src/forge/resolve.js +191 -0
- package/src/generators/adr.js +13 -4
- package/src/generators/date.js +23 -0
- package/src/generators/init.js +229 -39
- package/src/generators/note.js +154 -0
- package/src/generators/req.js +88 -5
- package/src/generators/roadmap.js +96 -20
- package/src/i18n/locales/en-US.json +2 -1
- package/src/i18n/locales/es-ES.json +2 -1
- package/src/i18n/locales/pt-BR.json +2 -1
- package/src/identity/preset.js +22 -0
- package/src/integrations/assets/agents/architect.md +35 -0
- package/src/integrations/assets/agents/backend.md +32 -0
- package/src/integrations/assets/agents/code-quality.md +32 -0
- package/src/integrations/assets/agents/data.md +32 -0
- package/src/integrations/assets/agents/dba.md +32 -0
- package/src/integrations/assets/agents/frontend.md +32 -0
- package/src/integrations/assets/agents/iac.md +46 -0
- package/src/integrations/assets/agents/infra.md +34 -0
- package/src/integrations/assets/agents/qa.md +32 -0
- package/src/integrations/assets/agents/security.md +32 -0
- package/src/integrations/assets/agents/tooling.md +47 -0
- package/src/integrations/assets/agents/ux.md +32 -0
- package/src/integrations/assets/catalog.json +16 -2
- package/src/integrations/assets/skills/architecture.md +66 -0
- package/src/integrations/assets/skills/backend.md +67 -0
- package/src/integrations/assets/skills/code-quality.md +78 -0
- package/src/integrations/assets/skills/data.md +75 -0
- package/src/integrations/assets/skills/dba.md +77 -0
- package/src/integrations/assets/skills/frontend.md +70 -0
- package/src/integrations/assets/skills/iac.md +90 -0
- package/src/integrations/assets/skills/infra.md +62 -0
- package/src/integrations/assets/skills/qa.md +63 -0
- package/src/integrations/assets/skills/security.md +74 -0
- package/src/integrations/assets/skills/tooling.md +80 -0
- package/src/integrations/assets/skills/ux.md +70 -0
- package/src/integrations/catalog.js +25 -1
- package/src/integrations/index.js +20 -2
- package/src/integrations/render.js +57 -6
- package/src/lib/update-engine.js +217 -0
- package/src/ship/runner.js +454 -0
- package/src/validator/index.js +323 -111
- package/src/validator/traceid.js +1 -1
package/src/commands/init.js
CHANGED
|
@@ -10,6 +10,7 @@ const cmd = new Command('init')
|
|
|
10
10
|
cmd.description(t('init.description'))
|
|
11
11
|
cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,kiro)', '')
|
|
12
12
|
cmd.option('--identity-preset <preset>', `Agent identity preset: none, neutral, ${identityStore.presetNames().join(', ')}`, 'none')
|
|
13
|
+
cmd.option('--forge <value>', 'Forge platform: github, gitlab, bitbucket, azure', '')
|
|
13
14
|
cmd.action(async (options, command) => {
|
|
14
15
|
const os = require('os')
|
|
15
16
|
const path = require('path')
|
|
@@ -29,6 +30,14 @@ cmd.action(async (options, command) => {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
// --forge: validate unconditionally above the non-TTY early return so that
|
|
34
|
+
// an invalid value fails loudly in CI instead of silently no-op'ing.
|
|
35
|
+
const forgeChanged = command.getOptionValueSource('forge') === 'cli'
|
|
36
|
+
if (forgeChanged && options.forge) {
|
|
37
|
+
const { validateForge } = require('../forge/resolve')
|
|
38
|
+
validateForge(options.forge) // throws on invalid value
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
// Pula o wizard de identidade inteiramente quando a flag foi passada
|
|
33
42
|
// explicitamente (já tratado acima) ou quando um arquivo de identidade já
|
|
34
43
|
// existe — re-executar init nunca deve sobrescrever silenciosamente uma
|
|
@@ -45,6 +54,7 @@ cmd.action(async (options, command) => {
|
|
|
45
54
|
pkgManager: 'npm',
|
|
46
55
|
hooks: 'none',
|
|
47
56
|
ci: 'none',
|
|
57
|
+
forge: forgeChanged ? options.forge : '',
|
|
48
58
|
}
|
|
49
59
|
await generators.scaffold(cfg)
|
|
50
60
|
const aiTools = String(options.aiTools || '').split(',').map(tool => tool.trim()).filter(Boolean)
|
|
@@ -62,7 +72,17 @@ cmd.action(async (options, command) => {
|
|
|
62
72
|
|
|
63
73
|
const { input, select, checkbox } = require('@inquirer/prompts')
|
|
64
74
|
|
|
65
|
-
|
|
75
|
+
// Detect forge from the current dir to prefill the wizard default.
|
|
76
|
+
const { resolveFromRepo: resolveForgeFromRepo } = require('../forge/resolve')
|
|
77
|
+
let detectedForge = ''
|
|
78
|
+
if (!forgeChanged) {
|
|
79
|
+
try {
|
|
80
|
+
const res = resolveForgeFromRepo('', '', process.cwd())
|
|
81
|
+
if (res.source !== 'none') detectedForge = res.forge
|
|
82
|
+
} catch (_) {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let projectName, projectType, frontend, pkgManager, backend, backendFramework, hooks, ci, forgeValue, aiTools, requireReqInCommit, scope
|
|
66
86
|
|
|
67
87
|
try {
|
|
68
88
|
projectName = await input({
|
|
@@ -163,6 +183,22 @@ cmd.action(async (options, command) => {
|
|
|
163
183
|
],
|
|
164
184
|
})
|
|
165
185
|
|
|
186
|
+
if (!forgeChanged) {
|
|
187
|
+
forgeValue = await select({
|
|
188
|
+
message: t('init.prompt.forge'),
|
|
189
|
+
default: detectedForge,
|
|
190
|
+
choices: [
|
|
191
|
+
{ name: 'Auto-detect (omit key)', value: '' },
|
|
192
|
+
{ name: 'GitHub', value: 'github' },
|
|
193
|
+
{ name: 'GitLab', value: 'gitlab' },
|
|
194
|
+
{ name: 'Bitbucket', value: 'bitbucket' },
|
|
195
|
+
{ name: 'Azure DevOps', value: 'azure' },
|
|
196
|
+
],
|
|
197
|
+
})
|
|
198
|
+
} else {
|
|
199
|
+
forgeValue = options.forge
|
|
200
|
+
}
|
|
201
|
+
|
|
166
202
|
requireReqInCommit = false
|
|
167
203
|
if (hooks !== 'none') {
|
|
168
204
|
const { confirm: confirmPrompt } = require('@inquirer/prompts')
|
|
@@ -202,6 +238,7 @@ cmd.action(async (options, command) => {
|
|
|
202
238
|
pkgManager: 'npm',
|
|
203
239
|
hooks: 'none',
|
|
204
240
|
ci: 'none',
|
|
241
|
+
forge: forgeChanged ? options.forge : '',
|
|
205
242
|
}
|
|
206
243
|
await generators.scaffold(cfg)
|
|
207
244
|
console.log(`\n${t('init.success')}`)
|
|
@@ -229,6 +266,7 @@ cmd.action(async (options, command) => {
|
|
|
229
266
|
pkgManager: 'npm',
|
|
230
267
|
hooks: 'none',
|
|
231
268
|
ci: 'none',
|
|
269
|
+
forge: forgeChanged ? options.forge : '',
|
|
232
270
|
})
|
|
233
271
|
console.log(`\n${t('init.success')}`)
|
|
234
272
|
require('../generators/init').printArchitectNextSteps(process.cwd())
|
|
@@ -236,7 +274,7 @@ cmd.action(async (options, command) => {
|
|
|
236
274
|
}
|
|
237
275
|
}
|
|
238
276
|
|
|
239
|
-
const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, requireReqInCommit }
|
|
277
|
+
const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, forge: forgeValue || '', requireReqInCommit }
|
|
240
278
|
await generators.scaffold(cfg)
|
|
241
279
|
|
|
242
280
|
for (const tool of (aiTools || [])) await generators.installIntegrationTarget(tool, process.cwd(), scope)
|
package/src/commands/log.js
CHANGED
|
@@ -3,13 +3,14 @@ const { Command } = require('commander')
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const { t } = require('../i18n')
|
|
6
|
+
const config = require('../config')
|
|
6
7
|
|
|
7
8
|
const cmd = new Command('log')
|
|
8
9
|
cmd.description(t('log.description'))
|
|
9
10
|
cmd.option('--tail <n>', t('log.tail'), '20')
|
|
10
11
|
cmd.action(async (opts) => {
|
|
11
12
|
const tail = parseInt(opts.tail, 10)
|
|
12
|
-
const logPath = path.join(
|
|
13
|
+
const logPath = path.join(config.load().roadmapDir, '.trackfw-log')
|
|
13
14
|
|
|
14
15
|
if (!fs.existsSync(logPath)) {
|
|
15
16
|
console.log(t('log.empty'))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const { newNote } = require('../generators/note')
|
|
5
|
+
|
|
6
|
+
function createNoteCommand() {
|
|
7
|
+
const note = new Command('note').description('Manage vault knowledge notes')
|
|
8
|
+
|
|
9
|
+
note
|
|
10
|
+
.command('new <title>')
|
|
11
|
+
.description('Create a new knowledge note in vault/notes/')
|
|
12
|
+
.action((title) => {
|
|
13
|
+
try {
|
|
14
|
+
newNote(title)
|
|
15
|
+
} catch (err) {
|
|
16
|
+
console.error(`error: ${err.message}`)
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return note
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = createNoteCommand()
|
package/src/commands/req.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const { Command } = require('commander')
|
|
3
|
-
const { listREQs } = require('../generators/req')
|
|
3
|
+
const { listREQs, moveREQ } = require('../generators/req')
|
|
4
4
|
const { t } = require('../i18n')
|
|
5
5
|
|
|
6
6
|
const cmd = new Command('req')
|
|
@@ -67,4 +67,10 @@ cmd.command('list')
|
|
|
67
67
|
listREQs(require('../config').load().reqDir)
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
+
cmd.command('move <name> <status>')
|
|
71
|
+
.description('Update a REQ status in place')
|
|
72
|
+
.action(async (name, status) => {
|
|
73
|
+
moveREQ(name, status)
|
|
74
|
+
})
|
|
75
|
+
|
|
70
76
|
module.exports = cmd
|
package/src/commands/roadmap.js
CHANGED
|
@@ -8,15 +8,16 @@ cmd.description(t('roadmap.description'))
|
|
|
8
8
|
|
|
9
9
|
cmd.command('new')
|
|
10
10
|
.description(t('roadmap.new.description'))
|
|
11
|
-
.
|
|
11
|
+
.argument('[title]', 'Roadmap title')
|
|
12
|
+
.option('-t, --title <title>', 'Roadmap title (alias)')
|
|
12
13
|
.option('-r, --req <path>', 'Path to the linked REQ')
|
|
13
14
|
.option('--from-req <path>', 'Generate roadmap with ML stubs from REQ acceptance criteria')
|
|
14
|
-
.action(async (opts) => {
|
|
15
|
+
.action(async (titleArg, opts) => {
|
|
15
16
|
if (opts.fromReq) {
|
|
16
17
|
newRoadmapFromReq(opts.fromReq)
|
|
17
18
|
return
|
|
18
19
|
}
|
|
19
|
-
const title = opts.title || 'New Roadmap'
|
|
20
|
+
const title = titleArg || opts.title || 'New Roadmap'
|
|
20
21
|
const reqPath = opts.req || ''
|
|
21
22
|
newRoadmap(title, reqPath)
|
|
22
23
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const { runShip } = require('../ship/runner')
|
|
5
|
+
const config = require('../config')
|
|
6
|
+
|
|
7
|
+
function createShipCommand() {
|
|
8
|
+
const cmd = new Command('ship')
|
|
9
|
+
cmd
|
|
10
|
+
.description(
|
|
11
|
+
'trackfw ship runs a governed delivery sequence:\n\n' +
|
|
12
|
+
' 1. Validates branch name — must match feat|fix|refactor/<slug>\n' +
|
|
13
|
+
' 2. Validates governance — REQ + roadmap in wip/ must exist\n' +
|
|
14
|
+
' (hard gate: not affected by lenient mode or per-rule severity)\n' +
|
|
15
|
+
' 3. Detects pending squash-merges in other branches (advisory only)\n' +
|
|
16
|
+
' 4. Reviews what is staged (git status --short + git diff --cached --stat)\n' +
|
|
17
|
+
' 5. Commits with Conventional Commits format (-m is required)\n' +
|
|
18
|
+
' 6. Pushes to origin (adds -u if no upstream is configured yet)\n' +
|
|
19
|
+
' 7. Opens PR/MR via the resolved forge CLI (or prints URL if CLI is absent)\n\n' +
|
|
20
|
+
"Stage your files explicitly before running ship.\n" +
|
|
21
|
+
"This command never executes 'git add .' or 'git add -A'."
|
|
22
|
+
)
|
|
23
|
+
.option('-m, --message <msg>', 'Commit message (Conventional Commits format required)')
|
|
24
|
+
.option('--dry-run', 'Print what would be done without executing write commands', false)
|
|
25
|
+
.option('--no-pr', 'Skip PR/MR creation after push')
|
|
26
|
+
.option('--forge <forge>', 'Override forge detection (github, gitlab, bitbucket, azure)', '')
|
|
27
|
+
.action((options) => {
|
|
28
|
+
const cfg = config.load()
|
|
29
|
+
const exitCode = runShip(
|
|
30
|
+
{
|
|
31
|
+
message: options.message || '',
|
|
32
|
+
dryRun: options.dryRun || false,
|
|
33
|
+
noPR: options.pr === false,
|
|
34
|
+
forge: options.forge || '',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
configForge: cfg.forge || '',
|
|
38
|
+
repoDir: '.',
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
if (exitCode !== 0) {
|
|
42
|
+
process.exit(exitCode)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return cmd
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = createShipCommand()
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const os = require('os')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const identityStore = require('../identity')
|
|
7
|
+
const { catalog, buildPlans, IntegrationManager, globalGroupPath } = require('../integrations')
|
|
8
|
+
const { tildeify, validateTargets, buildDocument, humanReport, silenceConsole } = require('../lib/update-engine')
|
|
9
|
+
|
|
10
|
+
// `trackfw update harness` is the global counterpart to `trackfw update` —
|
|
11
|
+
// see docs/cli-parity.md, "`trackfw update` vs `trackfw update harness`".
|
|
12
|
+
// It never requires trackfw.yaml or a project cwd, and its only job is to
|
|
13
|
+
// update rules/agents/skills ALREADY INSTALLED under the user's home
|
|
14
|
+
// directory (~/.claude and equivalents). It never installs a target that is
|
|
15
|
+
// not already present unless --install-missing is passed.
|
|
16
|
+
//
|
|
17
|
+
// Target universe (fixed declared order, mirrors internal/commands/
|
|
18
|
+
// update_harness.go and pypi/trackfw/commands/update_harness.py):
|
|
19
|
+
// 1. claude-skill — ~/.claude/skills/trackfw/SKILL.md (legacy
|
|
20
|
+
// governance meta-skill, previously written by the
|
|
21
|
+
// removed `update` step "skill global"; this is the
|
|
22
|
+
// exact target named in the frozen contract
|
|
23
|
+
// example in docs/cli-parity.md).
|
|
24
|
+
// 2..N. <tool>-agents / <tool>-skills — one target per (catalog target,
|
|
25
|
+
// kind) pair, in catalog.targets declared order,
|
|
26
|
+
// covering the project-independent, home-rooted
|
|
27
|
+
// agents/skills catalog (npm/src/integrations).
|
|
28
|
+
//
|
|
29
|
+
// Ambiguity (reported, not resolved unilaterally — see ML-6C handoff
|
|
30
|
+
// report): each <tool>-<kind> target aggregates potentially many catalog
|
|
31
|
+
// items (e.g. claude-agents bundles all 12 agent personas) into a single
|
|
32
|
+
// state. The contract does not specify item-level granularity for harness
|
|
33
|
+
// targets, so this implementation reports the bundle-level outcome:
|
|
34
|
+
// - every item not-installed -> missing
|
|
35
|
+
// - any item unmanaged/modified -> skipped (never overwritten)
|
|
36
|
+
// - otherwise, at least one item written -> updated
|
|
37
|
+
// - otherwise (all current) -> skipped
|
|
38
|
+
|
|
39
|
+
// claudeSkillContent — mirrors generators/init.js:installSkillsForce
|
|
40
|
+
// (npm/src/generators/init.js:1242) byte-for-byte. Duplicated here, not
|
|
41
|
+
// imported, because installSkillsForce() bakes os.homedir() internally with
|
|
42
|
+
// an unconditional overwrite and no dry-run mode — it cannot be reused
|
|
43
|
+
// without either mutating on --dry-run or forking its signature. Reported
|
|
44
|
+
// as a follow-up cleanup opportunity in the ML-6C handoff report.
|
|
45
|
+
function claudeSkillContent() {
|
|
46
|
+
return `---
|
|
47
|
+
name: trackfw
|
|
48
|
+
description: "trackfw — Governed Software Delivery: ADR → REQ → ROADMAP → kanban"
|
|
49
|
+
signature: "📦 trackfw - Governed Delivery"
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
# trackfw — Modo de Operação
|
|
53
|
+
|
|
54
|
+
Este projeto usa **trackfw** para governança de entrega de software.
|
|
55
|
+
Cadeia: **ADR → REQ → ROADMAP** · Estados: \`backlog / wip / blocked / done / abandoned\`
|
|
56
|
+
|
|
57
|
+
## Comandos principais
|
|
58
|
+
|
|
59
|
+
- \`trackfw context\` — contexto de trabalho atual (sempre execute primeiro)
|
|
60
|
+
- \`trackfw status\` — todos os artefatos e estados
|
|
61
|
+
- \`trackfw validate\` — valida consistência de governança
|
|
62
|
+
- \`trackfw roadmap move <nome> <estado>\` — transição de estado
|
|
63
|
+
- \`trackfw serve\` — board Kanban em http://localhost:4080
|
|
64
|
+
|
|
65
|
+
## Protocolo de agente
|
|
66
|
+
|
|
67
|
+
1. Antes de iniciar: \`trackfw context\` + ler \`docs/agents-working-context.md\`
|
|
68
|
+
2. Após concluir: atualizar \`docs/agents-working-context.md\`
|
|
69
|
+
3. Antes de PR: \`trackfw validate\` deve passar com zero violations
|
|
70
|
+
`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function claudeSkillTarget(homeRoot, { dryRun, installMissing }) {
|
|
74
|
+
const id = 'claude-skill'
|
|
75
|
+
const filePath = path.join(homeRoot, '.claude', 'skills', 'trackfw', 'SKILL.md')
|
|
76
|
+
const displayPath = tildeify(homeRoot, filePath)
|
|
77
|
+
const desired = claudeSkillContent()
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const exists = fs.existsSync(filePath)
|
|
81
|
+
const actual = exists ? fs.readFileSync(filePath, 'utf8') : null
|
|
82
|
+
|
|
83
|
+
if (!exists && !installMissing) return { id, state: 'missing', path: displayPath }
|
|
84
|
+
if (exists && actual === desired) return { id, state: 'skipped', path: displayPath }
|
|
85
|
+
|
|
86
|
+
if (dryRun) return { id, state: 'updated', path: displayPath }
|
|
87
|
+
|
|
88
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
89
|
+
fs.writeFileSync(filePath, desired, 'utf8')
|
|
90
|
+
return { id, state: 'updated', path: displayPath }
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// catalogBundleTarget — one target per (tool, kind) pair at global scope.
|
|
97
|
+
// Uses IntegrationManager.inspect (read-only) to classify every catalog
|
|
98
|
+
// item under that pair, then only calls manager.update() for the subset
|
|
99
|
+
// that needs a write — and never for --dry-run. displayPath is derived from
|
|
100
|
+
// the catalog itself (globalGroupPath), not from any individual plan's
|
|
101
|
+
// destination, so it never depends on catalog item iteration order (this is
|
|
102
|
+
// what previously caused the claude-skills path to diverge from the Python
|
|
103
|
+
// CLI — see docs/cli-parity.md, "Declared harness targets — pinned list").
|
|
104
|
+
function catalogBundleTarget(toolId, kind, homeRoot, identityConfig, { dryRun, installMissing }) {
|
|
105
|
+
const id = `${toolId}-${kind}`
|
|
106
|
+
let displayPath = `~/.${toolId}`
|
|
107
|
+
try {
|
|
108
|
+
displayPath = globalGroupPath(toolId, kind)
|
|
109
|
+
const plans = buildPlans(kind, { targets: [toolId], scope: 'global', identity: identityConfig })
|
|
110
|
+
if (!plans.length) return { id, state: 'missing', path: displayPath }
|
|
111
|
+
|
|
112
|
+
const manager = new IntegrationManager({ homeRoot })
|
|
113
|
+
const statuses = manager.inspect(plans)
|
|
114
|
+
|
|
115
|
+
const allNotInstalled = statuses.every((s) => s.state === 'not-installed')
|
|
116
|
+
const anyModified = statuses.some((s) => s.state === 'modified')
|
|
117
|
+
|
|
118
|
+
if (allNotInstalled && !installMissing) return { id, state: 'missing', path: displayPath }
|
|
119
|
+
|
|
120
|
+
const toWrite = plans.filter((_, index) => {
|
|
121
|
+
const state = statuses[index].state
|
|
122
|
+
if (state === 'outdated') return true
|
|
123
|
+
if (state === 'not-installed') return installMissing
|
|
124
|
+
return false
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (!toWrite.length) {
|
|
128
|
+
if (allNotInstalled) return { id, state: 'missing', path: displayPath }
|
|
129
|
+
return { id, state: 'skipped', path: displayPath }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!dryRun) manager.update(toWrite)
|
|
133
|
+
return { id, state: 'updated', path: displayPath }
|
|
134
|
+
} catch (e) {
|
|
135
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const HARNESS_TARGET_IDS = ['claude-skill']
|
|
140
|
+
for (const target of catalog.targets) {
|
|
141
|
+
HARNESS_TARGET_IDS.push(`${target.id}-agents`, `${target.id}-skills`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// buildHarnessTargets — `wanted` (nullable) restricts which targets are
|
|
145
|
+
// even computed/applied. This must happen HERE, not as a post-hoc filter
|
|
146
|
+
// on the returned array: catalogBundleTarget/claudeSkillTarget call
|
|
147
|
+
// manager.update() as a side effect, so filtering after construction would
|
|
148
|
+
// still have mutated every unrequested target's files on disk.
|
|
149
|
+
function buildHarnessTargets(homeRoot, identityConfig, { dryRun, installMissing }, wanted) {
|
|
150
|
+
const include = (id) => !wanted || wanted.includes(id)
|
|
151
|
+
const targets = []
|
|
152
|
+
if (include('claude-skill')) targets.push(claudeSkillTarget(homeRoot, { dryRun, installMissing }))
|
|
153
|
+
for (const target of catalog.targets) {
|
|
154
|
+
const agentsId = `${target.id}-agents`
|
|
155
|
+
const skillsId = `${target.id}-skills`
|
|
156
|
+
if (include(agentsId)) targets.push(catalogBundleTarget(target.id, 'agents', homeRoot, identityConfig, { dryRun, installMissing }))
|
|
157
|
+
if (include(skillsId)) targets.push(catalogBundleTarget(target.id, 'skills', homeRoot, identityConfig, { dryRun, installMissing }))
|
|
158
|
+
}
|
|
159
|
+
return targets
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// run — entry point invoked by `trackfw update harness`. Deliberately a
|
|
163
|
+
// plain function, not its own commander.Command: nesting a Command that
|
|
164
|
+
// redeclares the SAME flag names (--json, --dry-run, --targets,
|
|
165
|
+
// --install-missing) as its parent ('update') triggers a commander@12
|
|
166
|
+
// parsing quirk where the flag binds to the ANCESTOR command's opts()
|
|
167
|
+
// instead of the child's, silently producing `{}` in the child action no
|
|
168
|
+
// matter what was passed on the command line (reproduced and confirmed in
|
|
169
|
+
// isolation while building this ML — see the vault note this ML links).
|
|
170
|
+
// `update.js` instead parses `update harness --json` as ITS OWN single
|
|
171
|
+
// command with an optional positional `[mode]` argument, so there is only
|
|
172
|
+
// ever one Option object per flag name; it calls `run(options)` here when
|
|
173
|
+
// `mode === 'harness'`.
|
|
174
|
+
function run(options) {
|
|
175
|
+
let wanted
|
|
176
|
+
try {
|
|
177
|
+
const requested = options.targets ? String(options.targets).split(',').map((s) => s.trim()).filter(Boolean) : []
|
|
178
|
+
wanted = validateTargets(HARNESS_TARGET_IDS, requested)
|
|
179
|
+
} catch (e) {
|
|
180
|
+
console.error(`✗ ${e.message}`)
|
|
181
|
+
process.exit(1)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const homeRoot = os.homedir()
|
|
185
|
+
const dryRun = Boolean(options.dryRun)
|
|
186
|
+
const installMissing = Boolean(options.installMissing)
|
|
187
|
+
|
|
188
|
+
// Identidade resolvida do disco antes de buildPlans — pular esta etapa
|
|
189
|
+
// reverteria silenciosamente os nomes customizados para os defaults
|
|
190
|
+
// neutros (mesma justificativa de npm/src/integrations/index.js:execute).
|
|
191
|
+
const identityConfig = identityStore.load(homeRoot)
|
|
192
|
+
|
|
193
|
+
const targets = options.json
|
|
194
|
+
? silenceConsole(() => buildHarnessTargets(homeRoot, identityConfig, { dryRun, installMissing }, wanted))
|
|
195
|
+
: buildHarnessTargets(homeRoot, identityConfig, { dryRun, installMissing }, wanted)
|
|
196
|
+
|
|
197
|
+
const doc = buildDocument('harness', dryRun, targets)
|
|
198
|
+
|
|
199
|
+
if (options.json) {
|
|
200
|
+
console.log(JSON.stringify(doc, null, 2))
|
|
201
|
+
} else {
|
|
202
|
+
console.log(humanReport('harness', dryRun, targets))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (doc.summary.failed > 0) process.exitCode = 1
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { run, HARNESS_TARGET_IDS, buildHarnessTargets, claudeSkillContent }
|