trackfw 7.2.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/audit-surface.js +520 -0
- package/src/commands/barrier.js +107 -8
- package/src/commands/doctor.js +16 -3
- package/src/commands/index.js +1 -0
- package/src/commands/integrations.js +19 -2
- package/src/commands/thirdparty.js +7 -2
- package/src/commands/update-harness.js +3 -1
- package/src/commands/update.js +20 -1
- package/src/config/index.js +79 -0
- package/src/generators/hooks.js +18 -0
- package/src/generators/init.js +72 -16
- package/src/generators/roadmap.js +53 -2
- package/src/integrations/assets/agents/architect.md +5 -3
- package/src/integrations/assets/agents/security.md +3 -0
- package/src/integrations/scaffold_doctor.js +366 -0
- package/src/lib/update-engine.js +13 -1
package/src/generators/init.js
CHANGED
|
@@ -123,6 +123,10 @@ function generateValidateScript(cfg, cwd) {
|
|
|
123
123
|
const script = buildValidateScript(cfg)
|
|
124
124
|
const scriptPath = path.join(root, 'scripts', 'trackfw-validate.sh')
|
|
125
125
|
fs.writeFileSync(scriptPath, script, { encoding: 'utf8', mode: 0o755 })
|
|
126
|
+
// AC9 (REQ-2026-08-28): writeFileSync's {mode} option applies only on O_CREAT;
|
|
127
|
+
// for an existing file the mode is not changed. fs.chmodSync restores 0o755
|
|
128
|
+
// unconditionally, matching Python's os.chmod behavior (which was already correct).
|
|
129
|
+
fs.chmodSync(scriptPath, 0o755)
|
|
126
130
|
console.log(` ✓ ${path.join('scripts', 'trackfw-validate.sh')}`)
|
|
127
131
|
}
|
|
128
132
|
|
|
@@ -201,10 +205,13 @@ function generateCIWorkflow(cfg) {
|
|
|
201
205
|
}
|
|
202
206
|
}
|
|
203
207
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
// buildGitHubActionsWorkflowContent returns the byte-identical content the GitHub Actions
|
|
209
|
+
// workflow generator would write. Exported for scaffold doctor (ADR-2026-08-27): the
|
|
210
|
+
// comparison uses the same function the write path uses, so drift is structurally impossible.
|
|
211
|
+
// The cfg parameter is accepted for API symmetry with Go's buildGitHubActionsWorkflowContent
|
|
212
|
+
// (which also ignores cfg for now — the template is not cfg-dependent).
|
|
213
|
+
function buildGitHubActionsWorkflowContent(_cfg) {
|
|
214
|
+
return `name: trackfw-gate
|
|
208
215
|
on:
|
|
209
216
|
pull_request:
|
|
210
217
|
branches: [main]
|
|
@@ -222,14 +229,11 @@ jobs:
|
|
|
222
229
|
- name: Governance gate
|
|
223
230
|
run: trackfw validate
|
|
224
231
|
`
|
|
225
|
-
|
|
226
|
-
const filePath = '.github/workflows/trackfw-gate.yml'
|
|
227
|
-
fs.writeFileSync(filePath, content, 'utf8')
|
|
228
|
-
console.log(` ✓ ${filePath}`)
|
|
229
232
|
}
|
|
230
233
|
|
|
231
|
-
|
|
232
|
-
|
|
234
|
+
// buildGitLabCIWorkflowContent mirrors buildGitHubActionsWorkflowContent for GitLab CI.
|
|
235
|
+
function buildGitLabCIWorkflowContent(_cfg) {
|
|
236
|
+
return `# trackfw governance gate
|
|
233
237
|
trackfw-gate:
|
|
234
238
|
stage: test
|
|
235
239
|
image: alpine:latest
|
|
@@ -241,8 +245,17 @@ trackfw-gate:
|
|
|
241
245
|
only:
|
|
242
246
|
- merge_requests
|
|
243
247
|
`
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function generateGitHubActionsWorkflow() {
|
|
251
|
+
fs.mkdirSync('.github/workflows', { recursive: true })
|
|
252
|
+
const filePath = '.github/workflows/trackfw-gate.yml'
|
|
253
|
+
fs.writeFileSync(filePath, buildGitHubActionsWorkflowContent(null), 'utf8')
|
|
254
|
+
console.log(` ✓ ${filePath}`)
|
|
255
|
+
}
|
|
244
256
|
|
|
245
|
-
|
|
257
|
+
function generateGitLabCIWorkflow() {
|
|
258
|
+
fs.writeFileSync('.gitlab-ci-trackfw.yml', buildGitLabCIWorkflowContent(null), 'utf8')
|
|
246
259
|
console.log(' ✓ .gitlab-ci-trackfw.yml')
|
|
247
260
|
}
|
|
248
261
|
|
|
@@ -452,7 +465,7 @@ Delete the file when resolved. Visible as a live banner in \`trackfw serve\`.
|
|
|
452
465
|
- **Docker + .env from day 1:** containerize early; all config via env vars
|
|
453
466
|
- **2-layer validation:** frontend (UX) + backend (security) — never only one
|
|
454
467
|
- **API-first:** define OpenAPI contract before coding frontend/backend integration
|
|
455
|
-
- **
|
|
468
|
+
- **Threat model waves:** every feature roadmap opens with a Wave 0 threat model (before implementation) and closes with a red-team review wave (before release)
|
|
456
469
|
- **Test coverage:** TDD for critical logic; min 60% (prototype) / 80% (production)
|
|
457
470
|
- Use \`/trackfw:architect\` to define stack before the first REQ
|
|
458
471
|
` + conventionsSection + `
|
|
@@ -855,7 +868,7 @@ trackfw não está instalado. Instale com:
|
|
|
855
868
|
3. **Gerar o roadmap**
|
|
856
869
|
Com base no conteúdo da REQ, gere um roadmap seguindo **estritamente** este formato:
|
|
857
870
|
|
|
858
|
-
|
|
871
|
+
\`\`\`\`markdown
|
|
859
872
|
---
|
|
860
873
|
status: backlog
|
|
861
874
|
date: <YYYY-MM-DD>
|
|
@@ -870,6 +883,28 @@ trackfw não está instalado. Instale com:
|
|
|
870
883
|
## Diagnóstico / Contexto
|
|
871
884
|
<resumo do problema, motivação e escopo extraídos da REQ>
|
|
872
885
|
|
|
886
|
+
## Wave 0 — Threat Model
|
|
887
|
+
> Dependencies: none. Blocks all implementation.
|
|
888
|
+
|
|
889
|
+
### ML-0A — Threat model for this roadmap
|
|
890
|
+
**Status:** pending
|
|
891
|
+
**Files affected:**
|
|
892
|
+
**Actions:**
|
|
893
|
+
1. Enumeration completeness — is the list of surfaces in this roadmap complete? Name what is missing, or show the list is closed. Do not limit the search to the files already named by the REQ — before declaring the list closed, search the repository for other places that emit the same artifact or the same pattern (for example, grep for the literal the final artifact contains).
|
|
894
|
+
2. Threat model — who empties this Wave 0 without breaking any written rule, and how?
|
|
895
|
+
3. Falsification targets in both directions — for each surface, what breaks when the behavior regresses, and what breaks when it regresses the opposite way?
|
|
896
|
+
4. Declared residual — what this design accepts not covering.
|
|
897
|
+
**Acceptance criteria:**
|
|
898
|
+
- [ ] The four sections above answered with evidence, not a one-line assertion
|
|
899
|
+
- [ ] No implementation line written for this ML
|
|
900
|
+
|
|
901
|
+
**Gates da wave:**
|
|
902
|
+
\`\`\`bash
|
|
903
|
+
# Wave 0 gate — replace this placeholder with a project-specific check before
|
|
904
|
+
# marking ML-0A done. Do not remove the gate; replace its command (AC13).
|
|
905
|
+
exit 1 # placeholder gate fails closed until ML-0A replaces it — see docs/cli-parity.md
|
|
906
|
+
\`\`\`
|
|
907
|
+
|
|
873
908
|
## Wave 1 — <nome descritivo> (<N> MLs em paralelo)
|
|
874
909
|
> Dependências: Independente
|
|
875
910
|
|
|
@@ -890,7 +925,7 @@ trackfw não está instalado. Instale com:
|
|
|
890
925
|
## Wave 2 — <nome> (depende de Wave 1)
|
|
891
926
|
> Dependências: Wave 1 completa
|
|
892
927
|
...
|
|
893
|
-
|
|
928
|
+
\`\`\`\`
|
|
894
929
|
|
|
895
930
|
**Princípios obrigatórios:**
|
|
896
931
|
- MLs dentro da mesma Wave são **independentes** (arquivos distintos, sem conflito)
|
|
@@ -1032,9 +1067,19 @@ Próximo passo: abrir PR com gh pr create
|
|
|
1032
1067
|
|
|
1033
1068
|
Execute primeiro:
|
|
1034
1069
|
\`\`\`bash
|
|
1035
|
-
trackfw barrier <roadmap> --wave <n> --json
|
|
1070
|
+
trackfw barrier <roadmap> --wave <n> --trust-local-gates --json
|
|
1036
1071
|
\`\`\`
|
|
1037
1072
|
|
|
1073
|
+
\`--trust-local-gates\` é obrigatório aqui: roadmaps WIP (modificados localmente, ainda não commitados em
|
|
1074
|
+
\`origin/main\`) são marcados como não confiáveis pela CLI direta por padrão, como proteção contra a
|
|
1075
|
+
execução de gates de roadmaps chegados por PR de terceiro. O slash command aplica esse flag porque
|
|
1076
|
+
ele representa o fluxo legítimo do arquiteto operando no próprio repositório — não porque os gates
|
|
1077
|
+
são inspecionados previamente (o diff ainda é responsabilidade do checklist abaixo).
|
|
1078
|
+
|
|
1079
|
+
⚠️ **Não use \`--trust-local-gates\` ao revisar um roadmap chegado por PR de terceiro** — use a CLI
|
|
1080
|
+
direta sem o flag (\`trackfw barrier <roadmap> --wave <n> --json\`) para que os gates sejam marcados
|
|
1081
|
+
como \`not_evaluated\` e não executados.
|
|
1082
|
+
|
|
1038
1083
|
Este comando é **necessário mas não suficiente**. Ele verifica MLs concluídos, evidências e \`trackfw validate\`, mas não substitui as inspeções especializadas nem a auditoria de diff abaixo — nenhuma delas é avaliada pelo binário. Consulte a seção \`trackfw barrier\` em \`docs/cli-parity.md\` para o contrato completo (estados, exit codes, saída JSON).
|
|
1039
1084
|
|
|
1040
1085
|
Se o comando retornar exit code não-zero (\`blocked\` ou erro de resolução): pare, reporte a falha ao usuário e não prossiga no checklist até que a wave passe.
|
|
@@ -1258,8 +1303,11 @@ async function installAmazonQ(cwd = process.cwd()) {
|
|
|
1258
1303
|
*/
|
|
1259
1304
|
async function installIntegrationTarget(target, cwd = process.cwd(), scope = 'project', { onSkip } = {}) {
|
|
1260
1305
|
const { execute, buildPlans } = require('../integrations')
|
|
1306
|
+
const { resolveAgentModels } = require('../config')
|
|
1307
|
+
const { models: agentModels, warning: agentModelsWarning } = resolveAgentModels(scope, os.homedir(), cwd)
|
|
1308
|
+
if (agentModelsWarning) process.stderr.write(agentModelsWarning + '\n')
|
|
1261
1309
|
const roots = { projectRoot: cwd }
|
|
1262
|
-
const options = { targets: [target], scope, onSkip }
|
|
1310
|
+
const options = { targets: [target], scope, onSkip, agentModels }
|
|
1263
1311
|
// D5 — transparency: print resolved destinations before writing anything.
|
|
1264
1312
|
// buildPlans has no side effects, so it is safe to call here purely to
|
|
1265
1313
|
// enumerate destinations; the actual write happens below via execute().
|
|
@@ -1383,4 +1431,12 @@ module.exports = {
|
|
|
1383
1431
|
injectHooksDetected,
|
|
1384
1432
|
trackfwRulesBlock,
|
|
1385
1433
|
printArchitectNextSteps,
|
|
1434
|
+
// Template constants and builders — exported for scaffold doctor (ADR-2026-08-27).
|
|
1435
|
+
// Same single-source-of-truth principle as hooks.js exports: comparing against the
|
|
1436
|
+
// same object the write path uses makes drift structurally impossible.
|
|
1437
|
+
CLAUDE_COMMANDS,
|
|
1438
|
+
buildValidateScript,
|
|
1439
|
+
// CI workflow content builders (inline content from generateGitHub/GitLabCIWorkflow)
|
|
1440
|
+
buildGitHubActionsWorkflowContent,
|
|
1441
|
+
buildGitLabCIWorkflowContent,
|
|
1386
1442
|
}
|
|
@@ -5,6 +5,42 @@ const config = require('../config')
|
|
|
5
5
|
const { localDateISO } = require('./date')
|
|
6
6
|
const { resolveReqFiles } = require('../validator/index.js')
|
|
7
7
|
|
|
8
|
+
// wave0Block — "## Wave 0 — Threat Model" section prepended to every generated roadmap, before
|
|
9
|
+
// the first implementation wave (AC1, AC12). Byte-identical to internal/generators/roadmap.go's
|
|
10
|
+
// wave0Block/wave0GateFence and to pypi/trackfw/generators/roadmap.py's WAVE0_BLOCK (gate:
|
|
11
|
+
// scripts/check-artifact-parity.sh).
|
|
12
|
+
//
|
|
13
|
+
// The gate command (`exit 1`) is fixed, literal and never interpolated with any REQ title, slug
|
|
14
|
+
// or user-controlled string — see the Go generator's comment for the full rationale (AC13,
|
|
15
|
+
// docs/cli-parity.md § "trackfw barrier"). It intentionally fails closed until ML-0A replaces it
|
|
16
|
+
// with a real, project-specific check.
|
|
17
|
+
//
|
|
18
|
+
// The ML is always ML-0A, never ML-1A — newRoadmapFromReq labels MLs derived from REQ acceptance
|
|
19
|
+
// criteria "ML-1A", "ML-1B", ... starting at the first criterion.
|
|
20
|
+
const WAVE0_BLOCK = `## Wave 0 — Threat Model
|
|
21
|
+
> Dependencies: none. Blocks all implementation.
|
|
22
|
+
|
|
23
|
+
### ML-0A — Threat model for this roadmap
|
|
24
|
+
**Status:** pending
|
|
25
|
+
**Files affected:**
|
|
26
|
+
**Actions:**
|
|
27
|
+
1. Enumeration completeness — is the list of surfaces in this roadmap complete? Name what is missing, or show the list is closed. Do not limit the search to the files already named by the REQ — before declaring the list closed, search the repository for other places that emit the same artifact or the same pattern (for example, grep for the literal the final artifact contains).
|
|
28
|
+
2. Threat model — who empties this Wave 0 without breaking any written rule, and how?
|
|
29
|
+
3. Falsification targets in both directions — for each surface, what breaks when the behavior regresses, and what breaks when it regresses the opposite way?
|
|
30
|
+
4. Declared residual — what this design accepts not covering.
|
|
31
|
+
**Acceptance criteria:**
|
|
32
|
+
- [ ] The four sections above answered with evidence, not a one-line assertion
|
|
33
|
+
- [ ] No implementation line written for this ML
|
|
34
|
+
|
|
35
|
+
**Gates da wave:**
|
|
36
|
+
\`\`\`bash
|
|
37
|
+
# Wave 0 gate — replace this placeholder with a project-specific check before
|
|
38
|
+
# marking ML-0A done. Do not remove the gate; replace its command (AC13).
|
|
39
|
+
exit 1 # placeholder gate fails closed until ML-0A replaces it — see docs/cli-parity.md
|
|
40
|
+
\`\`\`
|
|
41
|
+
|
|
42
|
+
`
|
|
43
|
+
|
|
8
44
|
const VALID_STATES = ['backlog', 'analyzing', 'wip', 'blocked', 'done', 'abandoned']
|
|
9
45
|
const STATE_ORDER = ['analyzing', 'wip', 'backlog', 'blocked', 'done', 'abandoned']
|
|
10
46
|
const VALID_STATES_MESSAGE = VALID_STATES.join(', ')
|
|
@@ -400,6 +436,14 @@ function syncReqReferences(movedBasename, newRoadmapPath, cfg) {
|
|
|
400
436
|
* Em modo by_agent, usa o primeiro agente configurado.
|
|
401
437
|
*/
|
|
402
438
|
function newRoadmap(title, reqPath) {
|
|
439
|
+
// AC1/AC2: o título é dado de uma linha — newline e CR são entrada malformada.
|
|
440
|
+
// Mensagem byte-idêntica nos 3 CLIs (docs/cli-parity.md).
|
|
441
|
+
if (/[\n\r]/.test(title)) {
|
|
442
|
+
console.error('Error: roadmap title must be a single line: newline and carriage return are not allowed')
|
|
443
|
+
process.exitCode = 1
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
403
447
|
const cfg = config.load()
|
|
404
448
|
const date = localDateISO()
|
|
405
449
|
const slug = toSlug(title)
|
|
@@ -441,7 +485,7 @@ REQ: ${reqPath || ''}
|
|
|
441
485
|
- [ ]
|
|
442
486
|
- [ ]
|
|
443
487
|
|
|
444
|
-
## Wave 1 — <name> (parallel MLs)
|
|
488
|
+
${WAVE0_BLOCK}## Wave 1 — <name> (parallel MLs)
|
|
445
489
|
> Dependencies: none
|
|
446
490
|
|
|
447
491
|
### ML-1A — ${title}
|
|
@@ -476,6 +520,13 @@ function newRoadmapFromReq(reqPath) {
|
|
|
476
520
|
const basename = path.basename(reqPath)
|
|
477
521
|
const title = parsedTitle || basename.replace(/\.md$/, '').replace(/^REQ-/, '')
|
|
478
522
|
|
|
523
|
+
// AC1: o título lido da REQ também pode conter newline forjado — rejeitar cedo.
|
|
524
|
+
if (/[\n\r]/.test(title)) {
|
|
525
|
+
console.error('Error: roadmap title must be a single line: newline and carriage return are not allowed')
|
|
526
|
+
process.exitCode = 1
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
|
|
479
530
|
const cfg = config.load()
|
|
480
531
|
const date = localDateISO()
|
|
481
532
|
const slug = toSlug(title)
|
|
@@ -509,7 +560,7 @@ function newRoadmapFromReq(reqPath) {
|
|
|
509
560
|
mlLines.push('- [ ] build passes')
|
|
510
561
|
mlLines.push('- [ ] tests green')
|
|
511
562
|
}
|
|
512
|
-
const mlSection = mlLines.join('\n')
|
|
563
|
+
const mlSection = WAVE0_BLOCK + mlLines.join('\n')
|
|
513
564
|
|
|
514
565
|
const adrRef = linkedADR ? `\nADR: ${linkedADR}` : ''
|
|
515
566
|
|
|
@@ -24,16 +24,18 @@ Append an entry to `docs/agents-working-context.md` when you start and when you
|
|
|
24
24
|
Before investigating a bug or unexpected behavior, read `vault/notes/index.md` when it exists and open the related notes. After reaching a non-obvious root cause, write a note and link it in the index. Rule of thumb: if another agent would lose more than ten minutes tomorrow without the note, the note must exist.
|
|
25
25
|
|
|
26
26
|
## Git authority
|
|
27
|
-
`trackfw_architect` is the **only** role with Git authority in this project. No other agent creates branches, commits, pushes, checks out, merges or rebases — every specialist hands its work back to `trackfw_architect` uncommitted. This role: creates the branch with `trackfw branch new <type>/<slug>`, which validates a matching roadmap already sits in `wip/` or `done/` before running `git checkout -b` and blocks with governance orientation if none is found; falls back to a raw `git checkout -b` only if `trackfw branch new` is an unknown command (binary predating v6.4.0 or missing from PATH) or fails for a reason other than the expected missing-roadmap block, while still requiring REQ + roadmap in `wip` first; audits the full diff produced by specialists against the assigned scope; performs every commit, including orchestration artifacts (ADRs, REQs, roadmaps, vault notes, the working context file) and the product code produced by specialists once audited; pushes to the working branch
|
|
27
|
+
`trackfw_architect` is the **only** role with Git authority in this project. No other agent creates branches, commits, pushes, checks out, merges or rebases — every specialist hands its work back to `trackfw_architect` uncommitted. This role: creates the branch with `trackfw branch new <type>/<slug>`, which validates a matching roadmap already sits in `wip/` or `done/` before running `git checkout -b` and blocks with governance orientation if none is found; falls back to a raw `git checkout -b` only if `trackfw branch new` is an unknown command (binary predating v6.4.0 or missing from PATH) or fails for a reason other than the expected missing-roadmap block, while still requiring REQ + roadmap in `wip` first; audits the full diff produced by specialists against the assigned scope; performs every commit, including orchestration artifacts (ADRs, REQs, roadmaps, vault notes, the working context file) and the product code produced by specialists once audited; pushes commits already created to the working branch with `trackfw push`; and suggests opening a pull request, opening it only when the user explicitly asks. Never merge.
|
|
28
|
+
|
|
29
|
+
Three distinct commands, never interchangeable: `trackfw commit` commits staged changes on the working branch; `trackfw push` pushes commits already created — use it instead of a raw `git push`, which the guard intercepts and redirects here; `trackfw ship` composes commit + push + PR into one governed step. Prefer the raw `git commit`/`git push` only when `trackfw commit`/`trackfw push` are unknown commands (binary predating the version that introduced them) or fail for a reason unrelated to governance.
|
|
28
30
|
|
|
29
31
|
## Barrier protocol
|
|
30
|
-
Before releasing the next wave: invoke the `code-quality` and `security` roles whenever the change warrants their review (new code paths, dependencies, permissions, secrets, parsers, or attack surface); block the next wave on any failed check and dispatch a corrective microbatch to the owning role instead of proceeding; audit the diff and re-run the wave's gates before performing any commit. A green `trackfw barrier` result from the CLI is necessary but not sufficient — it does not replace the specialized code-quality/security review or the manual diff audit.
|
|
32
|
+
Before releasing the next wave: confirm Wave 0 (threat model) of the current roadmap is audited — every MB in it, before dispatching any implementation wave; invoke the `code-quality` and `security` roles whenever the change warrants their review (new code paths, dependencies, permissions, secrets, parsers, or attack surface); block the next wave on any failed check and dispatch a corrective microbatch to the owning role instead of proceeding; audit the diff and re-run the wave's gates before performing any commit. A green `trackfw barrier` result from the CLI is necessary but not sufficient — it does not replace the specialized code-quality/security review or the manual diff audit.
|
|
31
33
|
|
|
32
34
|
## Parallelization
|
|
33
35
|
Analyze real dependencies between microbatches before assigning work. Microbatches touching disjoint files run in parallel; microbatches sharing any file — including generated trees, build outputs and the git index — become sequential, and the reason is documented. Put an explicit barrier between waves. Every handoff prompt must be self-contained: exact files, exact values, exact commands. Never let two agents edit the same file at the same time.
|
|
34
36
|
|
|
35
37
|
## Workflow
|
|
36
|
-
Analyze the codebase and requirements; record material decisions in an ADR; create the REQ with an explicit negative scope; produce a roadmap of waves and microbatches with measurable acceptance criteria; create the branch; commit the governance artifacts before any handoff; dispatch the
|
|
38
|
+
Analyze the codebase and requirements; record material decisions in an ADR; create the REQ with an explicit negative scope; produce a roadmap of waves and microbatches with measurable acceptance criteria, starting with a Wave 0 threat model (`security` role, `trackfw barrier <roadmap> --wave 0`) that MUST be audited before any implementation wave is dispatched; create the branch; commit the governance artifacts before any handoff; dispatch Wave 0 first, then the implementation waves; audit each microbatch against its acceptance criteria; update the roadmap; open the pull request only on request.
|
|
37
39
|
|
|
38
40
|
## Dispatch contract
|
|
39
41
|
Naming a specialist in prose or in a roadmap's `squad:` field is documentation, not delegation — it does not route the Agent tool call by itself. Every dispatch to a specialist MUST pass the Agent tool's `subagent_type` parameter explicitly; omitting it silently falls back to the generic `general-purpose` agent, which has none of the intended specialist's domain instructions. The correct `subagent_type` value is the `name:` from the frontmatter of that role's installed agent file — always `<slug>-tf`, where `<slug>` depends on the identity the user configured (Greek, Norse, custom, or otherwise); never assume a fixed name. If the exact value is not already known, read the installed agent file for that role before dispatching instead of guessing from the name used in prose. Confirm `subagent_type` is present and correct before every dispatch call.
|
|
@@ -42,6 +42,9 @@ This role never executes Git operations — no `branch`, `commit`, `push`, `chec
|
|
|
42
42
|
## Definition of done
|
|
43
43
|
Green build and tests do not close a microbatch. It is done when the roadmap reflects the new status and the governance artifacts sit in the correct state folder. Leaving an artifact in the wrong folder is the failure the gate exists to catch.
|
|
44
44
|
|
|
45
|
+
## Wave 0 deliverable
|
|
46
|
+
When dispatched for a roadmap's `## Wave 0 — Threat Model`, produce a written threat model with exactly four sections, before any implementation line is written: **enumeration completeness** (is the list of surfaces in the roadmap complete? name what is missing, or show the list is closed); **threat model** (who empties this Wave 0 without breaking any written rule, and how — the adversary here is the rushed implementer and the optimistic architect, not an external attacker); **falsification targets in both directions** (for each surface: where the sabotage enters, which gate should catch it, in which direction — false negative and false positive); **declared residual** (what this design accepts not covering, said plainly). Mark each of the four sections done with evidence, not a one-line assertion — `trackfw barrier <roadmap> --wave 0` checks that the ML is `✅` and its acceptance criteria are `[x]`, never the quality of the analysis; a shallow Wave 0 can still pass those two checks, so the `trackfw_architect` audit of the artifact's content is what actually catches it.
|
|
47
|
+
|
|
45
48
|
## Mission
|
|
46
49
|
Perform evidence-backed threat analysis. Prioritize concrete exploit paths and mitigations, preserve authentication and least privilege, and never expose secrets or weaken controls to make a test pass.
|
|
47
50
|
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scaffold_doctor.js — scaffold artifact coverage for `trackfw doctor`
|
|
5
|
+
* (ADR-2026-08-27-doctor-cobre-artefatos-de-scaffold-por-comparacao-com-o-template-
|
|
6
|
+
* com-propriedade-dada-pelo-caminho.md).
|
|
7
|
+
*
|
|
8
|
+
* Mirrors internal/generators/scaffold_doctor.go (Go, canonical source of truth).
|
|
9
|
+
* Same design decisions apply — see that file's doc comment for the full rationale.
|
|
10
|
+
*
|
|
11
|
+
* Key decisions:
|
|
12
|
+
* - Property by path, not manifest (AC3): scaffold artifacts are identified by
|
|
13
|
+
* well-known namespace paths. No manifest entry is written or read.
|
|
14
|
+
* - Sibling classifier (AC15): scaffold artifacts are never in the manifest, so
|
|
15
|
+
* routing them through classifyDoctor would produce wrong remedies. The finding
|
|
16
|
+
* kinds SCAFFOLD_DIVERGENT / SCAFFOLD_MISSING are separate, with zero claim and
|
|
17
|
+
* a trackfw-update remedy.
|
|
18
|
+
* - Config-rendered templates (AC12): buildValidateScript(cfg) varies with
|
|
19
|
+
* cfg.backend/cfg.frontend — rendered from the project's own trackfw.yaml.
|
|
20
|
+
* - Eligibility for slash commands (AC14): only checked when
|
|
21
|
+
* .claude/commands/trackfw/ directory already exists.
|
|
22
|
+
* - Conditional artifacts (AC13): CI workflow only when cfg.ci declares it.
|
|
23
|
+
* - Neutral blame message (AC16): binary version stated; direction ambiguous.
|
|
24
|
+
* - Execute-bit checking (REQ-2026-08-28, AC2–AC5, AC10, AC11):
|
|
25
|
+
* The five scripts written with mode 0o755 are additionally checked for the
|
|
26
|
+
* owner-execute bit. The check uses (stat.mode & 0o100) !== 0 (not === 0o755)
|
|
27
|
+
* so umask-narrowed modes (0o750, 0o700) are accepted (AC10). Non-executable
|
|
28
|
+
* artifacts (slash commands, CI workflows) carry execBit=false and are never
|
|
29
|
+
* accused of a missing execute bit (AC4/AC11). Content divergence takes
|
|
30
|
+
* precedence over mode (at most one finding per artifact). Content correct +
|
|
31
|
+
* bit missing → SCAFFOLD_WRONG_MODE (AC3 distinct state). On Windows
|
|
32
|
+
* (process.platform === 'win32') the execute bit is not representable on NTFS,
|
|
33
|
+
* so the mode check is suppressed entirely — AC5.
|
|
34
|
+
*
|
|
35
|
+
* _platform is a module-level constant seeded from process.platform so that unit
|
|
36
|
+
* tests can override it via the exported _setPlatformForTest helper (AC5 testability).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const fs = require('fs')
|
|
40
|
+
const path = require('path')
|
|
41
|
+
const yaml = require('yaml')
|
|
42
|
+
|
|
43
|
+
const { SIGNAL_SCRIPT, CLEANUP_SCRIPT, CREDENTIAL_GUARD_SCRIPT, GIT_BRANCH_GUARD_SCRIPT } =
|
|
44
|
+
require('../generators/hooks')
|
|
45
|
+
const { CLAUDE_COMMANDS, buildValidateScript, buildGitHubActionsWorkflowContent, buildGitLabCIWorkflowContent } =
|
|
46
|
+
require('../generators/init')
|
|
47
|
+
|
|
48
|
+
// Finding kind constants — mirrors Go's DoctorScaffoldDivergent / DoctorScaffoldMissing /
|
|
49
|
+
// DoctorScaffoldWrongMode.
|
|
50
|
+
const SCAFFOLD_DIVERGENT = 'scaffold-divergent'
|
|
51
|
+
const SCAFFOLD_MISSING = 'scaffold-missing'
|
|
52
|
+
// SCAFFOLD_WRONG_MODE: content correct but owner-execute bit missing (AC3).
|
|
53
|
+
// Only emitted for artifacts with execBit=true; never on Windows (AC5).
|
|
54
|
+
const SCAFFOLD_WRONG_MODE = 'scaffold-wrong-mode'
|
|
55
|
+
|
|
56
|
+
// PYTHON_VALIDATE_SCRIPT_FORM is the byte-exact content Python's `trackfw init` and
|
|
57
|
+
// `trackfw update` (validate-script target) write to scripts/trackfw-validate.sh.
|
|
58
|
+
// Accepted by the set-membership check in checkValidateScriptArtifact so that a
|
|
59
|
+
// project initialized by the Python runtime does not produce a false-positive
|
|
60
|
+
// scaffold-divergent finding in the Node.js doctor.
|
|
61
|
+
//
|
|
62
|
+
// Scope of the exception: ONLY scripts/trackfw-validate.sh uses set-membership.
|
|
63
|
+
// All other scaffold artifacts use single-template equality.
|
|
64
|
+
// See docs/cli-parity.md, "validate.sh — pertencimento a conjunto (set-membership, escopado)".
|
|
65
|
+
const PYTHON_VALIDATE_SCRIPT_FORM = '#!/usr/bin/env bash\nset -euo pipefail\ntrackfw validate\n'
|
|
66
|
+
|
|
67
|
+
// Paths used by the scaffold doctor (mirrors Go's exported constants).
|
|
68
|
+
const CLAUDE_COMMANDS_DIR_PATH = '.claude/commands/trackfw'
|
|
69
|
+
const GITHUB_ACTIONS_WORKFLOW_PATH = '.github/workflows/trackfw-gate.yml'
|
|
70
|
+
const GITLAB_CI_WORKFLOW_PATH = '.gitlab-ci-trackfw.yml'
|
|
71
|
+
|
|
72
|
+
// _platform is seeded from process.platform at module load time.
|
|
73
|
+
// Tests override it via _setPlatformForTest to exercise the Windows guard (AC5).
|
|
74
|
+
let _platform = process.platform
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* _setPlatformForTest overrides the platform string used by mode checks.
|
|
78
|
+
* ONLY for use in unit tests. Returns a restore function.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} plat - e.g. 'win32' or 'darwin'
|
|
81
|
+
* @returns {function} call to restore the original value
|
|
82
|
+
*/
|
|
83
|
+
function _setPlatformForTest(plat) {
|
|
84
|
+
const prev = _platform
|
|
85
|
+
_platform = plat
|
|
86
|
+
return () => { _platform = prev }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* execBitPresent returns true when the file at absPath has the owner-execute bit set
|
|
91
|
+
* (stat.mode & 0o100 !== 0). Returns false on any stat error.
|
|
92
|
+
*
|
|
93
|
+
* Uses bit mask rather than equality to 0o755 so that umask-narrowed modes like
|
|
94
|
+
* 0o750 or 0o700 are also accepted — AC10.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} absPath - absolute path to the file
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
function execBitPresent(absPath) {
|
|
100
|
+
try {
|
|
101
|
+
const stat = fs.statSync(absPath)
|
|
102
|
+
return (stat.mode & 0o100) !== 0
|
|
103
|
+
} catch (_) {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* scaffoldRemedy returns a ready-to-copy remedy command for a scaffold finding.
|
|
110
|
+
* The message is neutral about blame direction (AC16).
|
|
111
|
+
*/
|
|
112
|
+
function scaffoldRemedy(action, relPath) {
|
|
113
|
+
// Lazy-require version to avoid a circular dependency at module load time.
|
|
114
|
+
let ver = 'unknown'
|
|
115
|
+
try {
|
|
116
|
+
const pkg = require('../../package.json')
|
|
117
|
+
ver = pkg.version || 'unknown'
|
|
118
|
+
} catch (_) {}
|
|
119
|
+
return `trackfw update # ${action} ${relPath}: content differs from the template trackfw v${ver} generates; if this project was initialized with a newer binary, update the binary instead`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* scaffoldWrongModeRemedy returns a remedy command for the scaffold-wrong-mode finding.
|
|
124
|
+
* Names the missing execute bit explicitly to distinguish it from content divergence (AC3).
|
|
125
|
+
* The message is runtime-neutral so the parity check can diff Go, Node, and Python outputs
|
|
126
|
+
* byte-for-byte on this finding kind.
|
|
127
|
+
*/
|
|
128
|
+
function scaffoldWrongModeRemedy(relPath) {
|
|
129
|
+
return `trackfw update # restore execute bit on ${relPath}: content is correct but the owner-execute bit is missing (mode 0755 required); trackfw update now restores the mode unconditionally on existing files`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* loadProjectConfig reads trackfw.yaml from projectRoot and returns a cfg object
|
|
134
|
+
* with the same keys that buildValidateScript / buildCIWorkflowContent expect.
|
|
135
|
+
* Missing keys default to undefined/null — identical to Go's loadUpdateConfig().
|
|
136
|
+
*/
|
|
137
|
+
function loadProjectConfig(projectRoot) {
|
|
138
|
+
try {
|
|
139
|
+
const raw = fs.readFileSync(path.join(projectRoot, 'trackfw.yaml'), 'utf8')
|
|
140
|
+
const parsed = yaml.parse(raw) || {}
|
|
141
|
+
return {
|
|
142
|
+
backend: parsed.backend || null,
|
|
143
|
+
frontend: parsed.frontend || null,
|
|
144
|
+
pkgManager: parsed.pkg_manager || null,
|
|
145
|
+
ci: parsed.ci || null,
|
|
146
|
+
}
|
|
147
|
+
} catch (_) {
|
|
148
|
+
return { backend: null, frontend: null, pkgManager: null, ci: null }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* checkValidateScriptArtifact checks scripts/trackfw-validate.sh using set-membership:
|
|
154
|
+
* accepted if the file content matches EITHER Go/Node's cfg-rendered form OR Python's
|
|
155
|
+
* fixed form (PYTHON_VALIDATE_SCRIPT_FORM). A file matching NO known form is accused.
|
|
156
|
+
* All other scaffold artifacts use checkScaffoldArtifact (single-template equality).
|
|
157
|
+
*
|
|
158
|
+
* After content membership passes, the execute bit is checked unless on Windows (AC5).
|
|
159
|
+
* Content divergence always takes precedence over mode (at most one finding per artifact).
|
|
160
|
+
*
|
|
161
|
+
* @param {string} absPath - absolute path to the file
|
|
162
|
+
* @param {string} relPath - relative path (used as destination in findings)
|
|
163
|
+
* @param {object} cfg - project config from trackfw.yaml
|
|
164
|
+
* @returns {object|null} finding or null
|
|
165
|
+
*/
|
|
166
|
+
function checkValidateScriptArtifact(absPath, relPath, cfg) {
|
|
167
|
+
let actual
|
|
168
|
+
try {
|
|
169
|
+
actual = fs.readFileSync(absPath, 'utf8')
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (err.code === 'ENOENT') {
|
|
172
|
+
return {
|
|
173
|
+
finding: SCAFFOLD_MISSING,
|
|
174
|
+
claim: { kind: '', item: '', target: '', surface: '', scope: '' },
|
|
175
|
+
destination: relPath,
|
|
176
|
+
remedy: scaffoldRemedy('restore', relPath),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
finding: SCAFFOLD_DIVERGENT,
|
|
181
|
+
claim: { kind: '', item: '', target: '', surface: '', scope: '' },
|
|
182
|
+
destination: relPath,
|
|
183
|
+
remedy: scaffoldRemedy('resync', relPath),
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const goNodeForm = buildValidateScript(cfg)
|
|
187
|
+
if (actual !== goNodeForm && actual !== PYTHON_VALIDATE_SCRIPT_FORM) {
|
|
188
|
+
// Content diverges — scaffold-divergent takes precedence over any mode issue.
|
|
189
|
+
return {
|
|
190
|
+
finding: SCAFFOLD_DIVERGENT,
|
|
191
|
+
claim: { kind: '', item: '', target: '', surface: '', scope: '' },
|
|
192
|
+
destination: relPath,
|
|
193
|
+
remedy: scaffoldRemedy('resync', relPath),
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Content accepted. Check the execute bit (AC2/AC3).
|
|
197
|
+
// Suppressed on Windows where the bit is not representable (AC5).
|
|
198
|
+
if (_platform !== 'win32' && !execBitPresent(absPath)) {
|
|
199
|
+
return {
|
|
200
|
+
finding: SCAFFOLD_WRONG_MODE,
|
|
201
|
+
claim: { kind: '', item: '', target: '', surface: '', scope: '' },
|
|
202
|
+
destination: relPath,
|
|
203
|
+
remedy: scaffoldWrongModeRemedy(relPath),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* checkScaffoldArtifact compares the on-disk content at absPath against expected.
|
|
211
|
+
* Returns a finding if the file is divergent or (when reportMissing=true) absent.
|
|
212
|
+
* relPath is used as destination in the finding.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} absPath - absolute path to the file
|
|
215
|
+
* @param {string} relPath - relative path (destination in findings)
|
|
216
|
+
* @param {string} expected - expected file content
|
|
217
|
+
* @param {boolean} reportMissing - if true, absence is a finding
|
|
218
|
+
* @param {boolean} execBit - if true, check the owner-execute bit after content passes
|
|
219
|
+
* true → artifact was written with 0o755; bit absence → SCAFFOLD_WRONG_MODE (AC2/AC3)
|
|
220
|
+
* false → artifact is 0o644 (slash commands, CI workflows); never mode-checked (AC4/AC11)
|
|
221
|
+
* @returns {object|null} finding or null
|
|
222
|
+
*/
|
|
223
|
+
function checkScaffoldArtifact(absPath, relPath, expected, reportMissing, execBit) {
|
|
224
|
+
let actual
|
|
225
|
+
try {
|
|
226
|
+
actual = fs.readFileSync(absPath, 'utf8')
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (err.code === 'ENOENT') {
|
|
229
|
+
if (!reportMissing) return null
|
|
230
|
+
return {
|
|
231
|
+
finding: SCAFFOLD_MISSING,
|
|
232
|
+
claim: { kind: '', item: '', target: '', surface: '', scope: '' },
|
|
233
|
+
destination: relPath,
|
|
234
|
+
remedy: scaffoldRemedy('restore', relPath),
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Unreadable artifact: report as divergent so the user is informed.
|
|
238
|
+
return {
|
|
239
|
+
finding: SCAFFOLD_DIVERGENT,
|
|
240
|
+
claim: { kind: '', item: '', target: '', surface: '', scope: '' },
|
|
241
|
+
destination: relPath,
|
|
242
|
+
remedy: scaffoldRemedy('resync', relPath),
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (actual !== expected) {
|
|
246
|
+
// Content diverges — takes precedence over any mode issue (at most one finding
|
|
247
|
+
// per artifact; update fixes both content and mode anyway).
|
|
248
|
+
return {
|
|
249
|
+
finding: SCAFFOLD_DIVERGENT,
|
|
250
|
+
claim: { kind: '', item: '', target: '', surface: '', scope: '' },
|
|
251
|
+
destination: relPath,
|
|
252
|
+
remedy: scaffoldRemedy('resync', relPath),
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Content matches. Check the execute bit when required (AC2/AC3).
|
|
256
|
+
// Suppressed on Windows where the bit is not representable (AC5).
|
|
257
|
+
if (execBit && _platform !== 'win32' && !execBitPresent(absPath)) {
|
|
258
|
+
return {
|
|
259
|
+
finding: SCAFFOLD_WRONG_MODE,
|
|
260
|
+
claim: { kind: '', item: '', target: '', surface: '', scope: '' },
|
|
261
|
+
destination: relPath,
|
|
262
|
+
remedy: scaffoldWrongModeRemedy(relPath),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* runScaffoldDoctor compares scaffold artifacts on disk against the templates the
|
|
270
|
+
* currently installed binary would generate (given the project's own trackfw.yaml),
|
|
271
|
+
* and returns findings for any artifact that is divergent or missing.
|
|
272
|
+
*
|
|
273
|
+
* @param {string} projectRoot - absolute path to the project root
|
|
274
|
+
* @returns {Array} findings
|
|
275
|
+
*/
|
|
276
|
+
function runScaffoldDoctor(projectRoot) {
|
|
277
|
+
// Eligibility: trackfw.yaml must exist.
|
|
278
|
+
try {
|
|
279
|
+
fs.statSync(path.join(projectRoot, 'trackfw.yaml'))
|
|
280
|
+
} catch (_) {
|
|
281
|
+
return []
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const cfg = loadProjectConfig(projectRoot)
|
|
285
|
+
const findings = []
|
|
286
|
+
|
|
287
|
+
// --- Scripts (always in scope when trackfw.yaml is present) ---
|
|
288
|
+
//
|
|
289
|
+
// scripts/trackfw-validate.sh uses set-membership (Go/Node form OR Python form).
|
|
290
|
+
// The four remaining scripts use single-template equality via checkScaffoldArtifact.
|
|
291
|
+
// All five have execBit=true: the generator writes them with mode 0o755 (AC11).
|
|
292
|
+
const validateF = checkValidateScriptArtifact(
|
|
293
|
+
path.join(projectRoot, 'scripts/trackfw-validate.sh'),
|
|
294
|
+
'scripts/trackfw-validate.sh',
|
|
295
|
+
cfg,
|
|
296
|
+
)
|
|
297
|
+
if (validateF) findings.push(validateF)
|
|
298
|
+
|
|
299
|
+
const staticScripts = [
|
|
300
|
+
{ relPath: 'scripts/trackfw-attention-signal.sh', expected: SIGNAL_SCRIPT, execBit: true },
|
|
301
|
+
{ relPath: 'scripts/trackfw-attention-cleanup.sh', expected: CLEANUP_SCRIPT, execBit: true },
|
|
302
|
+
{ relPath: 'scripts/trackfw-credential-guard.sh', expected: CREDENTIAL_GUARD_SCRIPT, execBit: true },
|
|
303
|
+
{ relPath: 'scripts/trackfw-git-branch-guard.sh', expected: GIT_BRANCH_GUARD_SCRIPT, execBit: true },
|
|
304
|
+
]
|
|
305
|
+
for (const { relPath, expected, execBit } of staticScripts) {
|
|
306
|
+
const f = checkScaffoldArtifact(path.join(projectRoot, relPath), relPath, expected, true, execBit)
|
|
307
|
+
if (f) findings.push(f)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --- Slash commands (AC14: only when the directory already exists) ---
|
|
311
|
+
//
|
|
312
|
+
// Slash commands are markdown files written with mode 0644 — execBit=false (AC11).
|
|
313
|
+
const claudeDir = path.join(projectRoot, CLAUDE_COMMANDS_DIR_PATH)
|
|
314
|
+
let claudeDirExists = false
|
|
315
|
+
try {
|
|
316
|
+
fs.statSync(claudeDir)
|
|
317
|
+
claudeDirExists = true
|
|
318
|
+
} catch (_) {}
|
|
319
|
+
if (claudeDirExists) {
|
|
320
|
+
for (const [filename, content] of Object.entries(CLAUDE_COMMANDS)) {
|
|
321
|
+
const relPath = `${CLAUDE_COMMANDS_DIR_PATH}/${filename}`
|
|
322
|
+
const f = checkScaffoldArtifact(path.join(projectRoot, relPath), relPath, content, true, false)
|
|
323
|
+
if (f) findings.push(f)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- CI workflow (AC13: conditional on ci: in trackfw.yaml) ---
|
|
328
|
+
//
|
|
329
|
+
// CI workflow YAML files are written with mode 0644 — execBit=false (AC11).
|
|
330
|
+
if (cfg.ci === 'github-actions') {
|
|
331
|
+
const relPath = GITHUB_ACTIONS_WORKFLOW_PATH
|
|
332
|
+
const f = checkScaffoldArtifact(
|
|
333
|
+
path.join(projectRoot, relPath),
|
|
334
|
+
relPath,
|
|
335
|
+
buildGitHubActionsWorkflowContent(cfg),
|
|
336
|
+
true,
|
|
337
|
+
false,
|
|
338
|
+
)
|
|
339
|
+
if (f) findings.push(f)
|
|
340
|
+
} else if (cfg.ci === 'gitlab-ci') {
|
|
341
|
+
const relPath = GITLAB_CI_WORKFLOW_PATH
|
|
342
|
+
const f = checkScaffoldArtifact(
|
|
343
|
+
path.join(projectRoot, relPath),
|
|
344
|
+
relPath,
|
|
345
|
+
buildGitLabCIWorkflowContent(cfg),
|
|
346
|
+
true,
|
|
347
|
+
false,
|
|
348
|
+
)
|
|
349
|
+
if (f) findings.push(f)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Deterministic output (AC7): sort by destination.
|
|
353
|
+
findings.sort((a, b) => (a.destination < b.destination ? -1 : a.destination > b.destination ? 1 : 0))
|
|
354
|
+
|
|
355
|
+
return findings
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = {
|
|
359
|
+
runScaffoldDoctor,
|
|
360
|
+
checkValidateScriptArtifact,
|
|
361
|
+
SCAFFOLD_DIVERGENT,
|
|
362
|
+
SCAFFOLD_MISSING,
|
|
363
|
+
SCAFFOLD_WRONG_MODE,
|
|
364
|
+
PYTHON_VALIDATE_SCRIPT_FORM,
|
|
365
|
+
_setPlatformForTest,
|
|
366
|
+
}
|