trackfw 7.1.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/audit-surface.js +520 -0
- package/src/commands/barrier.js +107 -8
- package/src/commands/commit.js +7 -3
- package/src/commands/doctor.js +17 -3
- package/src/commands/index.js +3 -0
- package/src/commands/integrations.js +80 -1
- package/src/commands/push.js +51 -0
- package/src/commands/release.js +47 -0
- package/src/commands/ship.js +10 -2
- package/src/commands/thirdparty.js +7 -2
- package/src/commands/update-harness.js +4 -1
- package/src/commands/update.js +21 -2
- package/src/config/index.js +96 -2
- package/src/generators/hooks.js +194 -13
- package/src/generators/init.js +84 -16
- package/src/generators/roadmap.js +53 -2
- package/src/integrations/assets/agents/architect.md +17 -3
- package/src/integrations/assets/agents/security.md +3 -0
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/index.js +1 -1
- package/src/integrations/render.js +116 -2
- package/src/integrations/scaffold_doctor.js +366 -0
- package/src/lib/update-engine.js +13 -1
- package/src/push/runner.js +223 -0
- package/src/release/runner.js +474 -0
- package/src/ship/runner.js +177 -21
- package/src/validator/index.js +270 -13
package/src/config/index.js
CHANGED
|
@@ -65,6 +65,10 @@ function defaults() {
|
|
|
65
65
|
credentialGuard: {
|
|
66
66
|
mode: 'warn',
|
|
67
67
|
},
|
|
68
|
+
// agent_models field — see ADR-2026-08-21-versao-do-modelo-por-tier-com-composicao-por-alvo.md.
|
|
69
|
+
// Maps tier name (e.g. "opus", "sonnet") to version string (e.g. "5", "4.6").
|
|
70
|
+
// Absent or empty map → behavior identical to today (tier alias used verbatim).
|
|
71
|
+
agentModels: {},
|
|
68
72
|
rules: {
|
|
69
73
|
wip_has_req: 'error',
|
|
70
74
|
wip_acceptance: 'error',
|
|
@@ -82,6 +86,21 @@ function defaults() {
|
|
|
82
86
|
|
|
83
87
|
let _instance = null;
|
|
84
88
|
|
|
89
|
+
// MALFORMED_GLOBAL_CONFIG_MESSAGE is written to stderr (non-fatal) when ~/.trackfw/trackfw.yaml
|
|
90
|
+
// exists but fails to parse as valid YAML. Must be byte-identical to Go's MalformedGlobalConfigMessage
|
|
91
|
+
// and Python's MALFORMED_GLOBAL_CONFIG_MESSAGE — ADR-2026-08-23.
|
|
92
|
+
const MALFORMED_GLOBAL_CONFIG_MESSAGE = 'trackfw: aviso: "~/.trackfw/trackfw.yaml" tem YAML malformado — config global de modelo ignorada; usando tier canônico.';
|
|
93
|
+
|
|
94
|
+
// GLOBAL_AGENT_MODELS_NONE_MESSAGE is emitted to stderr when agent_models is not configured
|
|
95
|
+
// in the global config and not found in cwd's trackfw.yaml either.
|
|
96
|
+
// Must be byte-identical to Go's GlobalAgentModelsNoneMessage and Python's equivalent.
|
|
97
|
+
const GLOBAL_AGENT_MODELS_NONE_MESSAGE = 'trackfw: agents global: agent_models não configurado em ~/.trackfw/trackfw.yaml — usando tier canônico. Configure em ~/.trackfw/trackfw.yaml para pinar versões.';
|
|
98
|
+
|
|
99
|
+
// GLOBAL_AGENT_MODELS_PROJECT_ONLY_MESSAGE is emitted when agent_models is found in the
|
|
100
|
+
// project's trackfw.yaml but not in the global config (AC4/AC14). Value NOT applied.
|
|
101
|
+
// Must be byte-identical to Go's GlobalAgentModelsProjectOnlyMessage and Python's equivalent.
|
|
102
|
+
const GLOBAL_AGENT_MODELS_PROJECT_ONLY_MESSAGE = 'trackfw: agents global: agent_models configurado em trackfw.yaml do projeto mas não vale para escopo global. Mova a chave para ~/.trackfw/trackfw.yaml.';
|
|
103
|
+
|
|
85
104
|
// MALFORMED_CONFIG_MESSAGE is written to stderr, verbatim, when trackfw.yaml exists but fails to
|
|
86
105
|
// parse as YAML. Kept identical, character-for-character, to Go's MalformedConfigMessage and
|
|
87
106
|
// Python's MALFORMED_CONFIG_MESSAGE — see the comment on parse() below for why the text is
|
|
@@ -278,6 +297,17 @@ function parse(content, cfg) {
|
|
|
278
297
|
if (mode === 'warn' || mode === 'block') cfg.credentialGuard.mode = mode;
|
|
279
298
|
}
|
|
280
299
|
|
|
300
|
+
// agent_models field — flat mapping from tier name to version string.
|
|
301
|
+
// See ADR-2026-08-21-versao-do-modelo-por-tier-com-composicao-por-alvo.md.
|
|
302
|
+
// An absent, malformed, or empty block leaves agentModels as the empty object from defaults(),
|
|
303
|
+
// preserving identical behavior to today. A key with an empty string value is stored as-is
|
|
304
|
+
// (the render layer treats empty string as "no pin" — fall back to tier alias).
|
|
305
|
+
if (m.agent_models !== undefined && typeof m.agent_models === 'object' && !Array.isArray(m.agent_models)) {
|
|
306
|
+
for (const [k, v] of Object.entries(m.agent_models)) {
|
|
307
|
+
if (typeof v === 'string') cfg.agentModels[k] = v;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
281
311
|
return false;
|
|
282
312
|
}
|
|
283
313
|
|
|
@@ -298,7 +328,7 @@ function parseRulesFromContent(content) {
|
|
|
298
328
|
// never creates them) — so this cannot be a bare { rules: {} } literal or parse() throws on any
|
|
299
329
|
// content that sets one of those nested keys. rules starts empty (not seeded from defaults()),
|
|
300
330
|
// deliberately: the caller wants exactly what `rules:` in content declares, nothing else.
|
|
301
|
-
const cfg = { rules: {}, credentialGuard: {}, update: {}, sync: {}, linkFields: {} };
|
|
331
|
+
const cfg = { rules: {}, credentialGuard: {}, update: {}, sync: {}, linkFields: {}, agentModels: {} };
|
|
302
332
|
parse(content, cfg);
|
|
303
333
|
return cfg.rules;
|
|
304
334
|
}
|
|
@@ -316,7 +346,7 @@ function readAgentConventions(cwd) {
|
|
|
316
346
|
} catch (_) {
|
|
317
347
|
return '';
|
|
318
348
|
}
|
|
319
|
-
const cfg = { rules: {}, credentialGuard: {}, update: { agentConventions: '' }, sync: {}, linkFields: {} };
|
|
349
|
+
const cfg = { rules: {}, credentialGuard: {}, update: { agentConventions: '' }, sync: {}, linkFields: {}, agentModels: {} };
|
|
320
350
|
try {
|
|
321
351
|
parse(content, cfg);
|
|
322
352
|
} catch (_) {
|
|
@@ -325,6 +355,65 @@ function readAgentConventions(cwd) {
|
|
|
325
355
|
return cfg.update.agentConventions || '';
|
|
326
356
|
}
|
|
327
357
|
|
|
358
|
+
// _cwdAgentModelsSource returns 'project_only' if cwd's trackfw.yaml has agent_models configured,
|
|
359
|
+
// 'none' otherwise. Used for the AC14 diagnostic in loadGlobalAgentModels.
|
|
360
|
+
function _cwdAgentModelsSource(cwd) {
|
|
361
|
+
if (!cwd) return 'none';
|
|
362
|
+
try {
|
|
363
|
+
const cwdContent = fs.readFileSync(path.join(cwd, 'trackfw.yaml'), 'utf8');
|
|
364
|
+
const cwdCfg = { rules: {}, credentialGuard: {}, update: {}, sync: {}, linkFields: {}, agentModels: {} };
|
|
365
|
+
parse(cwdContent, cwdCfg);
|
|
366
|
+
if (cwdCfg.agentModels && Object.keys(cwdCfg.agentModels).length > 0) return 'project_only';
|
|
367
|
+
} catch (_) {}
|
|
368
|
+
return 'none';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// loadGlobalAgentModels reads agent_models from ~/.trackfw/trackfw.yaml, bypassing the load()
|
|
372
|
+
// singleton. homeDir is os.homedir(); cwd is process.cwd() (used only for AC14 diagnostic).
|
|
373
|
+
// Returns { models: {}, source } where source is one of:
|
|
374
|
+
// 'global' — agent_models resolved from ~/.trackfw/trackfw.yaml
|
|
375
|
+
// 'none' — global config absent or has no agent_models; cwd also has none
|
|
376
|
+
// 'project_only' — agent_models in cwd's trackfw.yaml but not global (AC14 trigger)
|
|
377
|
+
// 'global_malformed' — ~/.trackfw/trackfw.yaml exists but has invalid YAML (AC12 trigger)
|
|
378
|
+
// Never calls process.exit. Pattern mirrors readAgentConventions above.
|
|
379
|
+
function loadGlobalAgentModels(homeDir, cwd) {
|
|
380
|
+
const globalPath = path.join(homeDir, '.trackfw', 'trackfw.yaml');
|
|
381
|
+
let data;
|
|
382
|
+
try {
|
|
383
|
+
data = fs.readFileSync(globalPath, 'utf8');
|
|
384
|
+
} catch (_) {
|
|
385
|
+
return { models: {}, source: _cwdAgentModelsSource(cwd) };
|
|
386
|
+
}
|
|
387
|
+
// Validate YAML (non-fatal for global config, per AC12).
|
|
388
|
+
const globalCfg = { rules: {}, credentialGuard: {}, update: {}, sync: {}, linkFields: {}, agentModels: {} };
|
|
389
|
+
const malformed = parse(data, globalCfg);
|
|
390
|
+
if (malformed) {
|
|
391
|
+
return { models: {}, source: 'global_malformed' };
|
|
392
|
+
}
|
|
393
|
+
if (globalCfg.agentModels && Object.keys(globalCfg.agentModels).length > 0) {
|
|
394
|
+
return { models: globalCfg.agentModels, source: 'global' };
|
|
395
|
+
}
|
|
396
|
+
// Global file exists but has no agent_models → check cwd (AC14).
|
|
397
|
+
return { models: {}, source: _cwdAgentModelsSource(cwd) };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// resolveAgentModels returns { models, warning } for the given scope.
|
|
401
|
+
// For global scope, reads from ~/.trackfw/trackfw.yaml via loadGlobalAgentModels.
|
|
402
|
+
// For project scope, reads from the cwd's trackfw.yaml via load().
|
|
403
|
+
// warning is a non-empty string when the caller should emit an advisory to stderr.
|
|
404
|
+
// resolveAgentModels never writes to stderr itself.
|
|
405
|
+
function resolveAgentModels(scope, homeDir, cwd) {
|
|
406
|
+
if (scope !== 'global') {
|
|
407
|
+
return { models: load().agentModels || {}, warning: '' };
|
|
408
|
+
}
|
|
409
|
+
const { models, source } = loadGlobalAgentModels(homeDir, cwd);
|
|
410
|
+
let warning = '';
|
|
411
|
+
if (source === 'none') warning = GLOBAL_AGENT_MODELS_NONE_MESSAGE;
|
|
412
|
+
else if (source === 'project_only') warning = GLOBAL_AGENT_MODELS_PROJECT_ONLY_MESSAGE;
|
|
413
|
+
else if (source === 'global_malformed') warning = MALFORMED_GLOBAL_CONFIG_MESSAGE;
|
|
414
|
+
return { models, warning };
|
|
415
|
+
}
|
|
416
|
+
|
|
328
417
|
const NAMESPACING_FLAT = 'flat';
|
|
329
418
|
const NAMESPACING_BY_AGENT = 'by_agent';
|
|
330
419
|
|
|
@@ -335,7 +424,12 @@ module.exports = {
|
|
|
335
424
|
expandPath,
|
|
336
425
|
parseRulesFromContent,
|
|
337
426
|
readAgentConventions,
|
|
427
|
+
loadGlobalAgentModels,
|
|
428
|
+
resolveAgentModels,
|
|
338
429
|
NAMESPACING_FLAT,
|
|
339
430
|
NAMESPACING_BY_AGENT,
|
|
340
431
|
MALFORMED_CONFIG_MESSAGE,
|
|
432
|
+
MALFORMED_GLOBAL_CONFIG_MESSAGE,
|
|
433
|
+
GLOBAL_AGENT_MODELS_NONE_MESSAGE,
|
|
434
|
+
GLOBAL_AGENT_MODELS_PROJECT_ONLY_MESSAGE,
|
|
341
435
|
};
|
package/src/generators/hooks.js
CHANGED
|
@@ -769,6 +769,24 @@ match_subcommand() {
|
|
|
769
769
|
;;
|
|
770
770
|
esac
|
|
771
771
|
done
|
|
772
|
+
# git checkout -- <path> | git checkout . descarta alterações não commitadas do
|
|
773
|
+
# caminho indicado, de forma irreversível, no worktree compartilhado — bloqueia
|
|
774
|
+
# quando '--' aparece em qualquer posição (forma explícita de pathspec) ou quando
|
|
775
|
+
# '.' aparece como token isolado. 'git checkout <branch>' sem nenhum dos dois
|
|
776
|
+
# segue liberado por decisão (distinguir branch de caminho sem '--' é ambíguo, e
|
|
777
|
+
# adivinhar produziria falso-positivo).
|
|
778
|
+
checkout_path=0
|
|
779
|
+
for tok2 in "$@"; do
|
|
780
|
+
case "$tok2" in
|
|
781
|
+
--|.)
|
|
782
|
+
checkout_path=1
|
|
783
|
+
;;
|
|
784
|
+
esac
|
|
785
|
+
done
|
|
786
|
+
if [ "$checkout_path" = "1" ]; then
|
|
787
|
+
echo "checkout-path"
|
|
788
|
+
return 0
|
|
789
|
+
fi
|
|
772
790
|
;;
|
|
773
791
|
switch)
|
|
774
792
|
for tok2 in "$@"; do
|
|
@@ -780,6 +798,87 @@ match_subcommand() {
|
|
|
780
798
|
esac
|
|
781
799
|
done
|
|
782
800
|
;;
|
|
801
|
+
stash)
|
|
802
|
+
# git stash: liberado só para leitura (list/show) — bloqueia a forma bare
|
|
803
|
+
# (equivale a "push"), push, save, clear e drop. Decisão de KG: bloquear a
|
|
804
|
+
# classe inteira, não só os literais medidos (ver REQ). Repositório com um único
|
|
805
|
+
# worktree compartilhado entre subagentes paralelos — um stash de um agente
|
|
806
|
+
# remove as alterações não commitadas de todos os outros.
|
|
807
|
+
stash_sub="\${1:-}"
|
|
808
|
+
case "$stash_sub" in
|
|
809
|
+
list|show)
|
|
810
|
+
;;
|
|
811
|
+
*)
|
|
812
|
+
echo "stash"
|
|
813
|
+
return 0
|
|
814
|
+
;;
|
|
815
|
+
esac
|
|
816
|
+
;;
|
|
817
|
+
reset)
|
|
818
|
+
# Só --hard bloqueia, em qualquer posição de token — --soft/--mixed (inclusive
|
|
819
|
+
# sem flag, que é --mixed implícito) seguem liberados: --soft é o contorno
|
|
820
|
+
# padrão para reempurrar trabalho staged via ` + "`" + `trackfw ship -m "..."` + "`" + ` (ainda falta commitar após --soft).
|
|
821
|
+
for tok2 in "$@"; do
|
|
822
|
+
case "$tok2" in
|
|
823
|
+
--hard)
|
|
824
|
+
echo "reset-hard"
|
|
825
|
+
return 0
|
|
826
|
+
;;
|
|
827
|
+
esac
|
|
828
|
+
done
|
|
829
|
+
;;
|
|
830
|
+
clean)
|
|
831
|
+
# Bloqueia qualquer forma com force (-f, -fd, -fx, --force) ou -x/-X, EXCETO
|
|
832
|
+
# quando -n/--dry-run também está presente (dry-run nunca apaga nada).
|
|
833
|
+
clean_dry=0
|
|
834
|
+
clean_force=0
|
|
835
|
+
for tok2 in "$@"; do
|
|
836
|
+
case "$tok2" in
|
|
837
|
+
-n|--dry-run)
|
|
838
|
+
clean_dry=1
|
|
839
|
+
;;
|
|
840
|
+
-f*|--force|--force=*|-x|-X)
|
|
841
|
+
clean_force=1
|
|
842
|
+
;;
|
|
843
|
+
esac
|
|
844
|
+
done
|
|
845
|
+
if [ "$clean_dry" != "1" ] && [ "$clean_force" = "1" ]; then
|
|
846
|
+
echo "clean-force"
|
|
847
|
+
return 0
|
|
848
|
+
fi
|
|
849
|
+
;;
|
|
850
|
+
restore)
|
|
851
|
+
# git restore --staged SOZINHO nunca toca o working tree (mexe só no
|
|
852
|
+
# index), então segue liberado mesmo com path. Mas --worktree/-W (com ou
|
|
853
|
+
# sem --staged junto) SEMPRE afeta o working tree — inclusive
|
|
854
|
+
# "--staged --worktree", que restaura os dois — então bloqueia sempre que
|
|
855
|
+
# --worktree/-W aparecer, e também no caso padrão (sem --staged em
|
|
856
|
+
# nenhuma forma) com um argumento posicional (o path).
|
|
857
|
+
restore_staged=0
|
|
858
|
+
restore_worktree=0
|
|
859
|
+
restore_positional=0
|
|
860
|
+
for tok2 in "$@"; do
|
|
861
|
+
case "$tok2" in
|
|
862
|
+
--staged)
|
|
863
|
+
restore_staged=1
|
|
864
|
+
;;
|
|
865
|
+
--worktree|-W)
|
|
866
|
+
restore_worktree=1
|
|
867
|
+
;;
|
|
868
|
+
-*)
|
|
869
|
+
;;
|
|
870
|
+
*)
|
|
871
|
+
restore_positional=1
|
|
872
|
+
;;
|
|
873
|
+
esac
|
|
874
|
+
done
|
|
875
|
+
if [ "$restore_positional" = "1" ]; then
|
|
876
|
+
if [ "$restore_worktree" = "1" ] || [ "$restore_staged" != "1" ]; then
|
|
877
|
+
echo "restore-path"
|
|
878
|
+
return 0
|
|
879
|
+
fi
|
|
880
|
+
fi
|
|
881
|
+
;;
|
|
783
882
|
branch)
|
|
784
883
|
# git branch é majoritariamente leitura (sem args, -a, -r, -l, --list, -v/-vv,
|
|
785
884
|
# --show-current, --contains, --no-contains, --merged, --no-merged, --sort=,
|
|
@@ -835,8 +934,44 @@ match_subcommand() {
|
|
|
835
934
|
;;
|
|
836
935
|
esac
|
|
837
936
|
done
|
|
937
|
+
elif [ "\${1:-}" = "remove" ]; then
|
|
938
|
+
# git worktree remove SEM -f/--force já recusa sozinho quando há alteração não
|
|
939
|
+
# commitada no worktree indicado — só a forma com force é irreversível o bastante
|
|
940
|
+
# para bloquear aqui.
|
|
941
|
+
shift
|
|
942
|
+
for tok2 in "$@"; do
|
|
943
|
+
case "$tok2" in
|
|
944
|
+
-f|--force)
|
|
945
|
+
echo "worktree-remove-force"
|
|
946
|
+
return 0
|
|
947
|
+
;;
|
|
948
|
+
esac
|
|
949
|
+
done
|
|
838
950
|
fi
|
|
839
951
|
;;
|
|
952
|
+
update-ref)
|
|
953
|
+
# git update-ref reescreve um ref (inclusive refs/remotes/origin/*) sem tocar o
|
|
954
|
+
# objeto apontado nem exigir push — foi o mecanismo que tornou alcançável o exploit
|
|
955
|
+
# descrito no ADR-2026-08-19-caminho-governado-para-push-forcado-e-tag-de-release.md
|
|
956
|
+
# (Emenda 1): forjar origin/<base> localmente para desviar o commit-alvo de trackfw
|
|
957
|
+
# release tag. Sem forma de leitura equivalente a bloquear seletivamente — a
|
|
958
|
+
# subcommand inteira é escrita — bloqueia sempre, sem exceção de token.
|
|
959
|
+
echo "update-ref"
|
|
960
|
+
return 0
|
|
961
|
+
;;
|
|
962
|
+
rm)
|
|
963
|
+
# git rm -f/--force apaga do working tree e do index de forma irreversível, mesma
|
|
964
|
+
# classe de git clean -f/git reset --hard já bloqueados acima — sem exceção para
|
|
965
|
+
# --cached (destrancar do index sem -f já segue liberado por não precisar de force).
|
|
966
|
+
for tok2 in "$@"; do
|
|
967
|
+
case "$tok2" in
|
|
968
|
+
-f*|--force|--force=*)
|
|
969
|
+
echo "rm-force"
|
|
970
|
+
return 0
|
|
971
|
+
;;
|
|
972
|
+
esac
|
|
973
|
+
done
|
|
974
|
+
;;
|
|
840
975
|
esac
|
|
841
976
|
done <<EOF
|
|
842
977
|
$normalized
|
|
@@ -848,22 +983,46 @@ SUBCOMMAND=$(match_subcommand "$CMD_RAW") || exit 0
|
|
|
848
983
|
|
|
849
984
|
case "$SUBCOMMAND" in
|
|
850
985
|
checkout-b)
|
|
851
|
-
REASON="trackfw: git checkout -b bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
986
|
+
REASON="trackfw: git checkout -b bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
852
987
|
;;
|
|
853
988
|
switch-c)
|
|
854
|
-
REASON="trackfw: git switch -c bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
989
|
+
REASON="trackfw: git switch -c bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
855
990
|
;;
|
|
856
991
|
branch-create)
|
|
857
|
-
REASON="trackfw: git branch bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
992
|
+
REASON="trackfw: git branch bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
858
993
|
;;
|
|
859
994
|
worktree-add-b)
|
|
860
|
-
REASON="trackfw: git worktree add -b bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
995
|
+
REASON="trackfw: git worktree add -b bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
861
996
|
;;
|
|
862
997
|
commit)
|
|
863
|
-
REASON="trackfw: git commit bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw commit -m '<mensagem>'` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
998
|
+
REASON="trackfw: git commit bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw commit -m '<mensagem>'` + GBG_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
864
999
|
;;
|
|
865
1000
|
push)
|
|
866
|
-
REASON="trackfw: git push bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw ship` + GBG_BACKTICK +
|
|
1001
|
+
REASON="trackfw: git push bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw push` + GBG_BACKTICK + ` (para empurrar commits já criados), ` + GBG_BACKTICK + `trackfw ship` + GBG_BACKTICK + ` (para commit+push+PR em uma etapa) ou ` + GBG_BACKTICK + `trackfw release tag` + GBG_BACKTICK + ` (para publicar uma tag de release). Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1002
|
+
;;
|
|
1003
|
+
stash)
|
|
1004
|
+
REASON="trackfw: git stash bruto bloqueado — worktree compartilhado entre subagentes, um stash remove as alterações não commitadas de todos os outros. ` + GBG_BACKTICK + `git stash list` + GBG_BACKTICK + `/` + GBG_BACKTICK + `git stash show` + GBG_BACKTICK + ` seguem liberados; para guardar trabalho em progresso, use uma branch própria via ` + GBG_BACKTICK + `trackfw branch new` + GBG_BACKTICK + ` e commit nela. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1005
|
+
;;
|
|
1006
|
+
reset-hard)
|
|
1007
|
+
REASON="trackfw: git reset --hard bruto bloqueado — descarta de forma irreversível as alterações não commitadas de todo o worktree compartilhado. ` + GBG_BACKTICK + `git reset --soft` + GBG_BACKTICK + `/` + GBG_BACKTICK + `--mixed` + GBG_BACKTICK + ` seguem liberados (ex.: ` + GBG_BACKTICK + `git reset --soft HEAD~1` + GBG_BACKTICK + ` é o caminho padrão; use ` + GBG_BACKTICK + `trackfw ship -m "..."` + GBG_BACKTICK + ` para commitar e empurrar). Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1008
|
+
;;
|
|
1009
|
+
clean-force)
|
|
1010
|
+
REASON="trackfw: git clean -f/-x bruto bloqueado — apaga arquivos não rastreados do worktree compartilhado, de forma irreversível. ` + GBG_BACKTICK + `git clean -n` + GBG_BACKTICK + `/` + GBG_BACKTICK + `--dry-run` + GBG_BACKTICK + ` segue liberado para revisar antes o que seria apagado. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1011
|
+
;;
|
|
1012
|
+
restore-path)
|
|
1013
|
+
REASON="trackfw: git restore <path> bruto bloqueado — descarta de forma irreversível as alterações não commitadas do caminho indicado. ` + GBG_BACKTICK + `git restore --staged` + GBG_BACKTICK + ` (não toca o working tree) segue liberado; para descartar de fato, confirme antes com o usuário. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1014
|
+
;;
|
|
1015
|
+
checkout-path)
|
|
1016
|
+
REASON="trackfw: git checkout -- <path>/git checkout . bruto bloqueado — descarta de forma irreversível as alterações não commitadas do caminho indicado. ` + GBG_BACKTICK + `git checkout <branch>` + GBG_BACKTICK + `/` + GBG_BACKTICK + `git switch <branch>` + GBG_BACKTICK + ` seguem liberados; para descartar de fato, confirme antes com o usuário. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1017
|
+
;;
|
|
1018
|
+
update-ref)
|
|
1019
|
+
REASON="trackfw: git update-ref bruto bloqueado — reescreve um ref (inclusive refs/remotes/origin/*) sem tocar o objeto apontado nem exigir push, o que permite forjar o commit-alvo que ` + GBG_BACKTICK + `trackfw release tag` + GBG_BACKTICK + ` publicaria. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1020
|
+
;;
|
|
1021
|
+
worktree-remove-force)
|
|
1022
|
+
REASON="trackfw: git worktree remove -f/--force bruto bloqueado — remove um worktree e descarta de forma irreversível qualquer alteração não commitada nele. ` + GBG_BACKTICK + `git worktree remove` + GBG_BACKTICK + ` sem force segue liberado (recusa sozinho quando há algo não commitado). Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1023
|
+
;;
|
|
1024
|
+
rm-force)
|
|
1025
|
+
REASON="trackfw: git rm -f/--force bruto bloqueado — apaga arquivos do working tree e do index de forma irreversível, mesma classe de ` + GBG_BACKTICK + `git clean -f` + GBG_BACKTICK + `/` + GBG_BACKTICK + `git reset --hard` + GBG_BACKTICK + ` já bloqueados. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
867
1026
|
;;
|
|
868
1027
|
*)
|
|
869
1028
|
exit 0
|
|
@@ -888,6 +1047,8 @@ function generateGitBranchGuardScript(cwd) {
|
|
|
888
1047
|
|
|
889
1048
|
const scriptPath = path.join(scriptsDir, 'trackfw-git-branch-guard.sh')
|
|
890
1049
|
fs.writeFileSync(scriptPath, GIT_BRANCH_GUARD_SCRIPT, { encoding: 'utf8', mode: 0o755 })
|
|
1050
|
+
// AC9: chmodSync restores execute bit on existing files.
|
|
1051
|
+
fs.chmodSync(scriptPath, 0o755)
|
|
891
1052
|
|
|
892
1053
|
console.log(' ✓ scripts/trackfw-git-branch-guard.sh')
|
|
893
1054
|
}
|
|
@@ -925,6 +1086,8 @@ function generateCredentialGuardScript(cwd) {
|
|
|
925
1086
|
|
|
926
1087
|
const scriptPath = path.join(scriptsDir, 'trackfw-credential-guard.sh')
|
|
927
1088
|
fs.writeFileSync(scriptPath, CREDENTIAL_GUARD_SCRIPT, { encoding: 'utf8', mode: 0o755 })
|
|
1089
|
+
// AC9: chmodSync restores execute bit on existing files.
|
|
1090
|
+
fs.chmodSync(scriptPath, 0o755)
|
|
928
1091
|
|
|
929
1092
|
console.log(' ✓ scripts/trackfw-credential-guard.sh')
|
|
930
1093
|
}
|
|
@@ -1330,9 +1493,14 @@ function generateAttentionScripts(cfg, cwd) {
|
|
|
1330
1493
|
|
|
1331
1494
|
const signalPath = path.join(scriptsDir, 'trackfw-attention-signal.sh')
|
|
1332
1495
|
fs.writeFileSync(signalPath, SIGNAL_SCRIPT, { encoding: 'utf8', mode: 0o755 })
|
|
1496
|
+
// AC9 (REQ-2026-08-28): chmodSync restores the execute bit on existing files
|
|
1497
|
+
// where writeFileSync's {mode} option has no effect (applies only on O_CREAT).
|
|
1498
|
+
fs.chmodSync(signalPath, 0o755)
|
|
1333
1499
|
|
|
1334
1500
|
const cleanupPath = path.join(scriptsDir, 'trackfw-attention-cleanup.sh')
|
|
1335
1501
|
fs.writeFileSync(cleanupPath, CLEANUP_SCRIPT, { encoding: 'utf8', mode: 0o755 })
|
|
1502
|
+
// AC9: same rationale as signalPath above.
|
|
1503
|
+
fs.chmodSync(cleanupPath, 0o755)
|
|
1336
1504
|
|
|
1337
1505
|
console.log(' ✓ scripts/trackfw-attention-signal.sh')
|
|
1338
1506
|
console.log(' ✓ scripts/trackfw-attention-cleanup.sh')
|
|
@@ -1965,7 +2133,17 @@ function injectWindsurfHooks(cwd) {
|
|
|
1965
2133
|
// written on first creation so the default agent keeps today's unrestricted
|
|
1966
2134
|
// tool access (this fix does not narrow what any agent can do, only where
|
|
1967
2135
|
// the deny wiring lives). Mirrors Go's InjectAmazonQHooks
|
|
1968
|
-
// (internal/generators/agentfiles.go) field-for-field
|
|
2136
|
+
// (internal/generators/agentfiles.go) field-for-field -- Go is the
|
|
2137
|
+
// canonical set (ROADMAP-2026-08-20, ML-1A-bis): only `name`, `description`
|
|
2138
|
+
// and `tools` are written on first creation. `prompt`/`mcpServers`/
|
|
2139
|
+
// `toolAliases`/`allowedTools`/`resources`/`useLegacyMcpJson` were written
|
|
2140
|
+
// here until ML-1A-bis and are now deliberately NOT written -- an extra
|
|
2141
|
+
// field the real schema doesn't expect risks failing validation, whereas an
|
|
2142
|
+
// absent optional field usually doesn't (asymmetry-of-risk decision, not a
|
|
2143
|
+
// verification against the live Amazon Q schema -- see docs/cli-parity.md
|
|
2144
|
+
// for the recorded limit). Pre-existing occurrences of the dropped fields
|
|
2145
|
+
// in a user's file are left untouched (never removed, only no longer
|
|
2146
|
+
// created).
|
|
1969
2147
|
//
|
|
1970
2148
|
// Native custom-agent toolset restriction (REQ acceptance criterion): still
|
|
1971
2149
|
// NOT implemented here -- this ML only wires the guard/deny fields on the
|
|
@@ -1982,13 +2160,7 @@ function injectAmazonQHooks(cwd) {
|
|
|
1982
2160
|
const defaults = {
|
|
1983
2161
|
name: 'q_cli_default',
|
|
1984
2162
|
description: 'trackfw-managed default agent — wires the git branch guard hook/denylist. See docs/cli-parity.md.',
|
|
1985
|
-
prompt: null,
|
|
1986
|
-
mcpServers: {},
|
|
1987
2163
|
tools: ['*'],
|
|
1988
|
-
toolAliases: {},
|
|
1989
|
-
allowedTools: [],
|
|
1990
|
-
resources: [],
|
|
1991
|
-
useLegacyMcpJson: false,
|
|
1992
2164
|
}
|
|
1993
2165
|
for (const [k, v] of Object.entries(defaults)) {
|
|
1994
2166
|
if (!Object.prototype.hasOwnProperty.call(data, k)) data[k] = v
|
|
@@ -2092,4 +2264,13 @@ module.exports = {
|
|
|
2092
2264
|
mergeCopilotHookArray,
|
|
2093
2265
|
normalizeGuardPath,
|
|
2094
2266
|
samePathCommand,
|
|
2267
|
+
// Template constants — exported for scaffold doctor (ADR-2026-08-27).
|
|
2268
|
+
// These are the single source of truth for each script's content; the
|
|
2269
|
+
// scaffold doctor compares on-disk files against these exact bytes, using
|
|
2270
|
+
// the same constants the write path uses, so drift between the comparison
|
|
2271
|
+
// and the generator is structurally impossible.
|
|
2272
|
+
SIGNAL_SCRIPT,
|
|
2273
|
+
CLEANUP_SCRIPT,
|
|
2274
|
+
CREDENTIAL_GUARD_SCRIPT,
|
|
2275
|
+
GIT_BRANCH_GUARD_SCRIPT,
|
|
2095
2276
|
}
|
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
|
+
}
|
|
244
249
|
|
|
245
|
-
|
|
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
|
+
}
|
|
256
|
+
|
|
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 + `
|
|
@@ -723,6 +736,18 @@ function generateClaudeMD(cfg) {
|
|
|
723
736
|
content += 'Decide low-risk details autonomously following existing project conventions, '
|
|
724
737
|
content += 'and record autonomous decisions in the commit message.\n'
|
|
725
738
|
|
|
739
|
+
content += '\n## Architect responses\n\n'
|
|
740
|
+
content += 'Default: what changed · what was decided · what is needed from you. Three to five lines.\n\n'
|
|
741
|
+
content += 'Scale up only on these three triggers, and only on them: a **blocker** that stops the next wave; '
|
|
742
|
+
content += 'a **pending user decision** that cannot be inferred from context; '
|
|
743
|
+
content += 'an **error the architect made** that cannot be self-corrected.\n\n'
|
|
744
|
+
content += 'Never cut, even when short: measured evidence (command and result), barrier verdict, decision taken and why. '
|
|
745
|
+
content += 'A response that buries a blocker in paragraph seven produced the same effect as not reporting it.\n\n'
|
|
746
|
+
content += 'Cut: restating what an executor already reported, re-explaining reasoning already given, '
|
|
747
|
+
content += 'recapping state that has not changed, closing praise. '
|
|
748
|
+
content += 'Tables and code blocks only when they replace prose, never when they add to it.\n\n'
|
|
749
|
+
content += 'Depth is on demand from the user.\n'
|
|
750
|
+
|
|
726
751
|
injectOrUpdateRules('CLAUDE.md', content, '.')
|
|
727
752
|
console.log(' ✓ CLAUDE.md')
|
|
728
753
|
}
|
|
@@ -843,7 +868,7 @@ trackfw não está instalado. Instale com:
|
|
|
843
868
|
3. **Gerar o roadmap**
|
|
844
869
|
Com base no conteúdo da REQ, gere um roadmap seguindo **estritamente** este formato:
|
|
845
870
|
|
|
846
|
-
|
|
871
|
+
\`\`\`\`markdown
|
|
847
872
|
---
|
|
848
873
|
status: backlog
|
|
849
874
|
date: <YYYY-MM-DD>
|
|
@@ -858,6 +883,28 @@ trackfw não está instalado. Instale com:
|
|
|
858
883
|
## Diagnóstico / Contexto
|
|
859
884
|
<resumo do problema, motivação e escopo extraídos da REQ>
|
|
860
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
|
+
|
|
861
908
|
## Wave 1 — <nome descritivo> (<N> MLs em paralelo)
|
|
862
909
|
> Dependências: Independente
|
|
863
910
|
|
|
@@ -878,7 +925,7 @@ trackfw não está instalado. Instale com:
|
|
|
878
925
|
## Wave 2 — <nome> (depende de Wave 1)
|
|
879
926
|
> Dependências: Wave 1 completa
|
|
880
927
|
...
|
|
881
|
-
|
|
928
|
+
\`\`\`\`
|
|
882
929
|
|
|
883
930
|
**Princípios obrigatórios:**
|
|
884
931
|
- MLs dentro da mesma Wave são **independentes** (arquivos distintos, sem conflito)
|
|
@@ -1020,9 +1067,19 @@ Próximo passo: abrir PR com gh pr create
|
|
|
1020
1067
|
|
|
1021
1068
|
Execute primeiro:
|
|
1022
1069
|
\`\`\`bash
|
|
1023
|
-
trackfw barrier <roadmap> --wave <n> --json
|
|
1070
|
+
trackfw barrier <roadmap> --wave <n> --trust-local-gates --json
|
|
1024
1071
|
\`\`\`
|
|
1025
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
|
+
|
|
1026
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).
|
|
1027
1084
|
|
|
1028
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.
|
|
@@ -1246,8 +1303,11 @@ async function installAmazonQ(cwd = process.cwd()) {
|
|
|
1246
1303
|
*/
|
|
1247
1304
|
async function installIntegrationTarget(target, cwd = process.cwd(), scope = 'project', { onSkip } = {}) {
|
|
1248
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')
|
|
1249
1309
|
const roots = { projectRoot: cwd }
|
|
1250
|
-
const options = { targets: [target], scope, onSkip }
|
|
1310
|
+
const options = { targets: [target], scope, onSkip, agentModels }
|
|
1251
1311
|
// D5 — transparency: print resolved destinations before writing anything.
|
|
1252
1312
|
// buildPlans has no side effects, so it is safe to call here purely to
|
|
1253
1313
|
// enumerate destinations; the actual write happens below via execute().
|
|
@@ -1371,4 +1431,12 @@ module.exports = {
|
|
|
1371
1431
|
injectHooksDetected,
|
|
1372
1432
|
trackfwRulesBlock,
|
|
1373
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,
|
|
1374
1442
|
}
|