safeword 0.77.0 → 0.78.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/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/hooks.json +5 -5
- package/dist/{architecture-WVYXN3C4.js → architecture-4EIJWLZ2.js} +6 -6
- package/dist/{architecture-document-7R3SGGE3.js → architecture-document-Z3GLUOE3.js} +6 -6
- package/dist/{architecture-monorepo-AP752JCP.js → architecture-monorepo-EIF3E3L3.js} +4 -4
- package/dist/{boundary-4SO7YSNO.js → boundary-U5U2PSX2.js} +9 -7
- package/dist/boundary-U5U2PSX2.js.map +1 -0
- package/dist/{chunk-JVZIT7JQ.js → chunk-34NCOTP5.js} +5 -12
- package/dist/chunk-34NCOTP5.js.map +1 -0
- package/dist/{chunk-YP2V3YUV.js → chunk-3EJNDIAO.js} +2 -2
- package/dist/{chunk-ASC7WZW3.js → chunk-3ETWYWGQ.js} +12 -7
- package/dist/{chunk-ASC7WZW3.js.map → chunk-3ETWYWGQ.js.map} +1 -1
- package/dist/{chunk-SEQUX642.js → chunk-4AIRSIOH.js} +206 -402
- package/dist/chunk-4AIRSIOH.js.map +1 -0
- package/dist/{chunk-NYTHQ2B5.js → chunk-4CZS4YJV.js} +3 -3
- package/dist/{chunk-ILCOG43E.js → chunk-664YNAMR.js} +42 -47
- package/dist/chunk-664YNAMR.js.map +1 -0
- package/dist/{chunk-S5HFJXWV.js → chunk-7KMQ3QPX.js} +60 -12
- package/dist/chunk-7KMQ3QPX.js.map +1 -0
- package/dist/{chunk-O2TTGWG6.js → chunk-A7Z27COA.js} +3 -3
- package/dist/{chunk-HJBS3QNE.js → chunk-AQU52TJM.js} +5 -5
- package/dist/{chunk-XHNWNZRM.js → chunk-AVAUUIDB.js} +2 -1
- package/dist/{chunk-XHNWNZRM.js.map → chunk-AVAUUIDB.js.map} +1 -1
- package/dist/{chunk-GMHP27HQ.js → chunk-AXAGJLMP.js} +19 -6
- package/dist/chunk-AXAGJLMP.js.map +1 -0
- package/dist/{chunk-CKPAHQBK.js → chunk-BAYWBZ32.js} +2 -2
- package/dist/{chunk-CKPAHQBK.js.map → chunk-BAYWBZ32.js.map} +1 -1
- package/dist/chunk-C377VEWH.js +836 -0
- package/dist/chunk-C377VEWH.js.map +1 -0
- package/dist/{chunk-LCOL7COA.js → chunk-E4UMSKEX.js} +371 -6
- package/dist/chunk-E4UMSKEX.js.map +1 -0
- package/dist/{chunk-FBLQAENR.js → chunk-FASTWBPY.js} +3 -3
- package/dist/chunk-FDHJ63FZ.js +241 -0
- package/dist/chunk-FDHJ63FZ.js.map +1 -0
- package/dist/{chunk-2MI7M2CO.js → chunk-GKR44BCH.js} +2 -2
- package/dist/{chunk-PZTGLQSD.js → chunk-I4KBHRZW.js} +4 -4
- package/dist/{chunk-OB6Q2Z5P.js → chunk-JZFZWY3K.js} +2 -2
- package/dist/{chunk-LXUBCIMR.js → chunk-KFVEPQ7X.js} +2 -2
- package/dist/{chunk-URVJZ4NL.js → chunk-Q5KLE6ZF.js} +2 -2
- package/dist/{chunk-PWBSTTNZ.js → chunk-QUDIOBHP.js} +2 -2
- package/dist/{chunk-ZU4BADKV.js → chunk-RXQJH4LM.js} +55 -52
- package/dist/chunk-RXQJH4LM.js.map +1 -0
- package/dist/{chunk-7G6OSNIV.js → chunk-S2T356DA.js} +12 -1
- package/dist/chunk-S2T356DA.js.map +1 -0
- package/dist/{chunk-XP6HSKIO.js → chunk-SLRSGK2C.js} +23 -11
- package/dist/{chunk-XP6HSKIO.js.map → chunk-SLRSGK2C.js.map} +1 -1
- package/dist/{chunk-K64BTVJ7.js → chunk-USPVNZ63.js} +4 -4
- package/dist/{chunk-76N32NR7.js → chunk-VUWQD4AG.js} +12 -13
- package/dist/chunk-VUWQD4AG.js.map +1 -0
- package/dist/chunk-W32GLIFH.js +384 -0
- package/dist/chunk-W32GLIFH.js.map +1 -0
- package/dist/{cleanup-PEKTOSU4.js → cleanup-QV2EBF6M.js} +8 -8
- package/dist/{cleanup-command-PWSZHMHU.js → cleanup-command-DTE64KTN.js} +16 -21
- package/dist/cleanup-command-DTE64KTN.js.map +1 -0
- package/dist/cli.js +67 -67
- package/dist/{clients-CVKGCTZW.js → clients-N4UXDPCN.js} +2 -2
- package/dist/{codex-bootstrap-CUJMNR2O.js → codex-bootstrap-7OUYHP4G.js} +12 -11
- package/dist/{codex-bootstrap-CUJMNR2O.js.map → codex-bootstrap-7OUYHP4G.js.map} +1 -1
- package/dist/{codex-hook-VFWWWZO5.js → codex-hook-VHGSPJN3.js} +14 -9
- package/dist/codex-hook-VHGSPJN3.js.map +1 -0
- package/dist/{codify-Q4QTOEYR.js → codify-DJELEKJ6.js} +6 -6
- package/dist/{commands-6Q2ZHYXD.js → commands-4NIKNAST.js} +383 -78
- package/dist/commands-4NIKNAST.js.map +1 -0
- package/dist/{config-YS3MWN2S.js → config-5A4LJ62T.js} +2 -2
- package/dist/{configured-paths-422KCT22.js → configured-paths-ZY2GRPYU.js} +4 -4
- package/dist/{contract-ZANMKQUA.js → contract-ZRRRAQWZ.js} +2 -2
- package/dist/{coordinator-KHKBXC2X.js → coordinator-ZPEXUBXT.js} +42 -9
- package/dist/coordinator-ZPEXUBXT.js.map +1 -0
- package/dist/corpus-ZJQNIU4R.js +15 -0
- package/dist/{doctor-2F4MZOKW.js → doctor-HUUE7QWE.js} +18 -18
- package/dist/{feature-directories-EJHOSKBZ.js → feature-directories-7ACGMPS4.js} +6 -6
- package/dist/{finalization-DAFOASTI.js → finalization-5D5N64R7.js} +4 -4
- package/dist/{gh-cli-5XBL6Q5O.js → gh-cli-46VSKW2O.js} +2 -2
- package/dist/{github-rest-SLMHJKTN.js → github-rest-SXLBFCQP.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{job-JU6C3MNE.js → job-EW5LWBHH.js} +2 -2
- package/dist/{learning-sync-CSLFG5B4.js → learning-sync-DRQM2JF6.js} +4 -4
- package/dist/{legacy-global-guidance-UKPPC5IF.js → legacy-global-guidance-DWGAGL44.js} +2 -2
- package/dist/{lint-gherkin-OU5E5FAX.js → lint-gherkin-5LJX6BBU.js} +6 -6
- package/dist/{operations-IJX2OQRX.js → operations-5CJI6U6T.js} +12 -11
- package/dist/{output-5GJEMIR7.js → output-5TOP54O5.js} +2 -2
- package/dist/{packet-3XYLZLNL.js → packet-IHZNYM3G.js} +2 -2
- package/dist/presets/typescript/index.js +1 -1
- package/dist/profile-ZZGSSTSJ.js +23 -0
- package/dist/{prompt-327TLOCO.js → prompt-C3MNQKAT.js} +2 -2
- package/dist/remove-PRBJCO6G.js +22 -0
- package/dist/{retro-B3WWVSY6.js → retro-Z2DREW77.js} +8 -8
- package/dist/{retro-draft-spool-G4JHZYMQ.js → retro-draft-spool-YVNO4VSB.js} +2 -2
- package/dist/{retro-extract-7YVZE2N4.js → retro-extract-NBSIY4ZY.js} +2 -2
- package/dist/{review-pr-7FETZMCE.js → review-pr-TTJKQC4H.js} +2 -2
- package/dist/{review-pr-publication-CWWPWO5O.js → review-pr-publication-DR75RPF4.js} +2 -2
- package/dist/{run-D4H4SR7F.js → run-5JWZAFET.js} +4 -4
- package/dist/{self-report-RM7S4MGJ.js → self-report-3KBA5TG7.js} +2 -2
- package/dist/{status-NU2GKW7P.js → status-K62NWMZ6.js} +18 -18
- package/dist/status-OFXGT5ZO.js +21 -0
- package/dist/{sync-config-ZEGCKOGQ.js → sync-config-B62CJ5OS.js} +5 -5
- package/dist/{sync-tracker-OBMROIKQ.js → sync-tracker-QMPXXLJ4.js} +6 -6
- package/dist/{test-execution-JBOVYXQY.js → test-execution-MCMXLV5O.js} +5 -5
- package/dist/{test-plan-ILRZYQPZ.js → test-plan-RJTKHEYA.js} +4 -4
- package/dist/{ticket-new-II4RX5AZ.js → ticket-new-RPLN56XK.js} +4 -4
- package/dist/{ticket-sync-ADIS26WY.js → ticket-sync-VHTAPISH.js} +5 -5
- package/dist/{tracker-map-JLGKQ7U7.js → tracker-map-WFRDFSKN.js} +2 -2
- package/dist/{tracker-sync-T6GVAOP3.js → tracker-sync-5D4XNQHS.js} +2 -2
- package/package.json +2 -2
- package/templates/hooks/dependency-bootstrap.ts +31 -0
- package/templates/hooks/lib/closeout-binding.ts +140 -1
- package/templates/hooks/lib/dependency-readiness.ts +222 -9
- package/templates/hooks/lib/safeword-context.ts +27 -13
- package/templates/hooks/session-codex-start.ts +7 -1
- package/templates/hooks/session-compact-context.ts +2 -2
- package/templates/hooks/session-dependency-readiness.ts +10 -94
- package/templates/scripts/closeout-cleanup.ts +258 -30
- package/dist/boundary-4SO7YSNO.js.map +0 -1
- package/dist/chunk-76N32NR7.js.map +0 -1
- package/dist/chunk-7G6OSNIV.js.map +0 -1
- package/dist/chunk-GMHP27HQ.js.map +0 -1
- package/dist/chunk-ILCOG43E.js.map +0 -1
- package/dist/chunk-JVZIT7JQ.js.map +0 -1
- package/dist/chunk-LCOL7COA.js.map +0 -1
- package/dist/chunk-LN7QZNRU.js +0 -445
- package/dist/chunk-LN7QZNRU.js.map +0 -1
- package/dist/chunk-S5HFJXWV.js.map +0 -1
- package/dist/chunk-SEQUX642.js.map +0 -1
- package/dist/chunk-UHRE3WGS.js +0 -94
- package/dist/chunk-UHRE3WGS.js.map +0 -1
- package/dist/chunk-ZU4BADKV.js.map +0 -1
- package/dist/chunk-ZYFJJH5K.js +0 -52
- package/dist/chunk-ZYFJJH5K.js.map +0 -1
- package/dist/cleanup-command-PWSZHMHU.js.map +0 -1
- package/dist/codex-hook-VFWWWZO5.js.map +0 -1
- package/dist/commands-6Q2ZHYXD.js.map +0 -1
- package/dist/coordinator-KHKBXC2X.js.map +0 -1
- package/dist/corpus-CSANEUWT.js +0 -15
- package/dist/profile-NZ535SJU.js +0 -32
- package/dist/remove-W5RFBHZC.js +0 -21
- package/dist/status-4UPPGHU7.js +0 -25
- /package/dist/{architecture-WVYXN3C4.js.map → architecture-4EIJWLZ2.js.map} +0 -0
- /package/dist/{architecture-document-7R3SGGE3.js.map → architecture-document-Z3GLUOE3.js.map} +0 -0
- /package/dist/{architecture-monorepo-AP752JCP.js.map → architecture-monorepo-EIF3E3L3.js.map} +0 -0
- /package/dist/{chunk-YP2V3YUV.js.map → chunk-3EJNDIAO.js.map} +0 -0
- /package/dist/{chunk-NYTHQ2B5.js.map → chunk-4CZS4YJV.js.map} +0 -0
- /package/dist/{chunk-O2TTGWG6.js.map → chunk-A7Z27COA.js.map} +0 -0
- /package/dist/{chunk-HJBS3QNE.js.map → chunk-AQU52TJM.js.map} +0 -0
- /package/dist/{chunk-FBLQAENR.js.map → chunk-FASTWBPY.js.map} +0 -0
- /package/dist/{chunk-2MI7M2CO.js.map → chunk-GKR44BCH.js.map} +0 -0
- /package/dist/{chunk-PZTGLQSD.js.map → chunk-I4KBHRZW.js.map} +0 -0
- /package/dist/{chunk-OB6Q2Z5P.js.map → chunk-JZFZWY3K.js.map} +0 -0
- /package/dist/{chunk-LXUBCIMR.js.map → chunk-KFVEPQ7X.js.map} +0 -0
- /package/dist/{chunk-URVJZ4NL.js.map → chunk-Q5KLE6ZF.js.map} +0 -0
- /package/dist/{chunk-PWBSTTNZ.js.map → chunk-QUDIOBHP.js.map} +0 -0
- /package/dist/{chunk-K64BTVJ7.js.map → chunk-USPVNZ63.js.map} +0 -0
- /package/dist/{cleanup-PEKTOSU4.js.map → cleanup-QV2EBF6M.js.map} +0 -0
- /package/dist/{clients-CVKGCTZW.js.map → clients-N4UXDPCN.js.map} +0 -0
- /package/dist/{codify-Q4QTOEYR.js.map → codify-DJELEKJ6.js.map} +0 -0
- /package/dist/{config-YS3MWN2S.js.map → config-5A4LJ62T.js.map} +0 -0
- /package/dist/{configured-paths-422KCT22.js.map → configured-paths-ZY2GRPYU.js.map} +0 -0
- /package/dist/{contract-ZANMKQUA.js.map → contract-ZRRRAQWZ.js.map} +0 -0
- /package/dist/{corpus-CSANEUWT.js.map → corpus-ZJQNIU4R.js.map} +0 -0
- /package/dist/{doctor-2F4MZOKW.js.map → doctor-HUUE7QWE.js.map} +0 -0
- /package/dist/{feature-directories-EJHOSKBZ.js.map → feature-directories-7ACGMPS4.js.map} +0 -0
- /package/dist/{finalization-DAFOASTI.js.map → finalization-5D5N64R7.js.map} +0 -0
- /package/dist/{gh-cli-5XBL6Q5O.js.map → gh-cli-46VSKW2O.js.map} +0 -0
- /package/dist/{github-rest-SLMHJKTN.js.map → github-rest-SXLBFCQP.js.map} +0 -0
- /package/dist/{job-JU6C3MNE.js.map → job-EW5LWBHH.js.map} +0 -0
- /package/dist/{learning-sync-CSLFG5B4.js.map → learning-sync-DRQM2JF6.js.map} +0 -0
- /package/dist/{legacy-global-guidance-UKPPC5IF.js.map → legacy-global-guidance-DWGAGL44.js.map} +0 -0
- /package/dist/{lint-gherkin-OU5E5FAX.js.map → lint-gherkin-5LJX6BBU.js.map} +0 -0
- /package/dist/{operations-IJX2OQRX.js.map → operations-5CJI6U6T.js.map} +0 -0
- /package/dist/{output-5GJEMIR7.js.map → output-5TOP54O5.js.map} +0 -0
- /package/dist/{packet-3XYLZLNL.js.map → packet-IHZNYM3G.js.map} +0 -0
- /package/dist/{profile-NZ535SJU.js.map → profile-ZZGSSTSJ.js.map} +0 -0
- /package/dist/{prompt-327TLOCO.js.map → prompt-C3MNQKAT.js.map} +0 -0
- /package/dist/{remove-W5RFBHZC.js.map → remove-PRBJCO6G.js.map} +0 -0
- /package/dist/{retro-B3WWVSY6.js.map → retro-Z2DREW77.js.map} +0 -0
- /package/dist/{retro-draft-spool-G4JHZYMQ.js.map → retro-draft-spool-YVNO4VSB.js.map} +0 -0
- /package/dist/{retro-extract-7YVZE2N4.js.map → retro-extract-NBSIY4ZY.js.map} +0 -0
- /package/dist/{review-pr-7FETZMCE.js.map → review-pr-TTJKQC4H.js.map} +0 -0
- /package/dist/{review-pr-publication-CWWPWO5O.js.map → review-pr-publication-DR75RPF4.js.map} +0 -0
- /package/dist/{run-D4H4SR7F.js.map → run-5JWZAFET.js.map} +0 -0
- /package/dist/{self-report-RM7S4MGJ.js.map → self-report-3KBA5TG7.js.map} +0 -0
- /package/dist/{status-4UPPGHU7.js.map → status-K62NWMZ6.js.map} +0 -0
- /package/dist/{status-NU2GKW7P.js.map → status-OFXGT5ZO.js.map} +0 -0
- /package/dist/{sync-config-ZEGCKOGQ.js.map → sync-config-B62CJ5OS.js.map} +0 -0
- /package/dist/{sync-tracker-OBMROIKQ.js.map → sync-tracker-QMPXXLJ4.js.map} +0 -0
- /package/dist/{test-execution-JBOVYXQY.js.map → test-execution-MCMXLV5O.js.map} +0 -0
- /package/dist/{test-plan-ILRZYQPZ.js.map → test-plan-RJTKHEYA.js.map} +0 -0
- /package/dist/{ticket-new-II4RX5AZ.js.map → ticket-new-RPLN56XK.js.map} +0 -0
- /package/dist/{ticket-sync-ADIS26WY.js.map → ticket-sync-VHTAPISH.js.map} +0 -0
- /package/dist/{tracker-map-JLGKQ7U7.js.map → tracker-map-WFRDFSKN.js.map} +0 -0
- /package/dist/{tracker-sync-T6GVAOP3.js.map → tracker-sync-5D4XNQHS.js.map} +0 -0
package/codex-plugin/hooks.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "bunx --bun safeword@0.
|
|
9
|
+
"command": "bunx --bun safeword@0.78.0 hook codex session-start --plugin-hook",
|
|
10
10
|
"timeout": 120,
|
|
11
11
|
"statusMessage": "Loading Safeword standing instructions"
|
|
12
12
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"hooks": [
|
|
20
20
|
{
|
|
21
21
|
"type": "command",
|
|
22
|
-
"command": "bunx --bun safeword@0.
|
|
22
|
+
"command": "bunx --bun safeword@0.78.0 hook codex pre-tool-use --plugin-hook",
|
|
23
23
|
"timeout": 30,
|
|
24
24
|
"statusMessage": "Checking Safeword edit gates"
|
|
25
25
|
}
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"hooks": [
|
|
33
33
|
{
|
|
34
34
|
"type": "command",
|
|
35
|
-
"command": "bunx --bun safeword@0.
|
|
35
|
+
"command": "bunx --bun safeword@0.78.0 hook codex post-tool-use --plugin-hook",
|
|
36
36
|
"timeout": 30,
|
|
37
37
|
"statusMessage": "Surfacing Safeword post-tool context"
|
|
38
38
|
}
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"hooks": [
|
|
46
46
|
{
|
|
47
47
|
"type": "command",
|
|
48
|
-
"command": "bunx --bun safeword@0.
|
|
48
|
+
"command": "bunx --bun safeword@0.78.0 hook codex user-prompt-submit --plugin-hook",
|
|
49
49
|
"timeout": 30,
|
|
50
50
|
"statusMessage": "Checking queued Safeword prompt context"
|
|
51
51
|
}
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"hooks": [
|
|
59
59
|
{
|
|
60
60
|
"type": "command",
|
|
61
|
-
"command": "bunx --bun safeword@0.
|
|
61
|
+
"command": "bunx --bun safeword@0.78.0 hook codex stop --plugin-hook",
|
|
62
62
|
"timeout": 600,
|
|
63
63
|
"statusMessage": "Checking Safeword stop continuation"
|
|
64
64
|
}
|
|
@@ -9,23 +9,23 @@ import {
|
|
|
9
9
|
planSelfHealProject,
|
|
10
10
|
selfHealProject,
|
|
11
11
|
selfHealProjectPreservingProse
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-I4KBHRZW.js";
|
|
13
13
|
import {
|
|
14
14
|
discoverUnreadableWorkspaces,
|
|
15
15
|
extractMonorepoArchitectureSnapshot
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-3EJNDIAO.js";
|
|
17
17
|
import {
|
|
18
18
|
GENERATED_ARCHITECTURE_FILENAME,
|
|
19
19
|
isArchitectureDocumentEnforcementEnabled,
|
|
20
20
|
readConfiguredPath,
|
|
21
21
|
resolveGeneratedArchitecturePath,
|
|
22
22
|
toRepoDirectory
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-GKR44BCH.js";
|
|
24
24
|
import {
|
|
25
25
|
readJson,
|
|
26
26
|
writeJson
|
|
27
|
-
} from "./chunk-
|
|
28
|
-
import "./chunk-
|
|
27
|
+
} from "./chunk-S2T356DA.js";
|
|
28
|
+
import "./chunk-BAYWBZ32.js";
|
|
29
29
|
|
|
30
30
|
// src/commands/architecture.ts
|
|
31
31
|
import { execFileSync } from "child_process";
|
|
@@ -650,4 +650,4 @@ export {
|
|
|
650
650
|
architectureStage,
|
|
651
651
|
architectureStaged
|
|
652
652
|
};
|
|
653
|
-
//# sourceMappingURL=architecture-
|
|
653
|
+
//# sourceMappingURL=architecture-4EIJWLZ2.js.map
|
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
selfHeal,
|
|
8
8
|
selfHealProject,
|
|
9
9
|
selfHealProjectPreservingProse
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
10
|
+
} from "./chunk-I4KBHRZW.js";
|
|
11
|
+
import "./chunk-3EJNDIAO.js";
|
|
12
|
+
import "./chunk-GKR44BCH.js";
|
|
13
|
+
import "./chunk-S2T356DA.js";
|
|
14
|
+
import "./chunk-BAYWBZ32.js";
|
|
15
15
|
export {
|
|
16
16
|
isSafewordOwned,
|
|
17
17
|
isWouldChangeAction,
|
|
@@ -22,4 +22,4 @@ export {
|
|
|
22
22
|
selfHealProject,
|
|
23
23
|
selfHealProjectPreservingProse
|
|
24
24
|
};
|
|
25
|
-
//# sourceMappingURL=architecture-document-
|
|
25
|
+
//# sourceMappingURL=architecture-document-Z3GLUOE3.js.map
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
discoverWorkspaces,
|
|
4
4
|
extractMonorepoArchitectureSnapshot,
|
|
5
5
|
monorepoFingerprintOf
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-3EJNDIAO.js";
|
|
7
|
+
import "./chunk-S2T356DA.js";
|
|
8
|
+
import "./chunk-BAYWBZ32.js";
|
|
9
9
|
export {
|
|
10
10
|
discoverUnreadableWorkspaces,
|
|
11
11
|
discoverWorkspaces,
|
|
12
12
|
extractMonorepoArchitectureSnapshot,
|
|
13
13
|
monorepoFingerprintOf
|
|
14
14
|
};
|
|
15
|
-
//# sourceMappingURL=architecture-monorepo-
|
|
15
|
+
//# sourceMappingURL=architecture-monorepo-EIF3E3L3.js.map
|
|
@@ -7,25 +7,25 @@ import {
|
|
|
7
7
|
} from "./chunk-3D25MSWD.js";
|
|
8
8
|
import {
|
|
9
9
|
createPhaseAnchorScope
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-USPVNZ63.js";
|
|
11
11
|
import {
|
|
12
12
|
classifyAnnotation,
|
|
13
13
|
isValidSha,
|
|
14
14
|
isValidSkipReason,
|
|
15
15
|
parseCheckboxAnnotation
|
|
16
16
|
} from "./chunk-Z4ESH6KU.js";
|
|
17
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-Q5KLE6ZF.js";
|
|
18
18
|
import {
|
|
19
19
|
warn
|
|
20
20
|
} from "./chunk-QSLVO4XF.js";
|
|
21
21
|
import {
|
|
22
22
|
readConfiguredPathFromContent,
|
|
23
23
|
ticketDirectoriesForConfiguredRoot
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-GKR44BCH.js";
|
|
25
25
|
import {
|
|
26
26
|
readFileSafe
|
|
27
|
-
} from "./chunk-
|
|
28
|
-
import "./chunk-
|
|
27
|
+
} from "./chunk-S2T356DA.js";
|
|
28
|
+
import "./chunk-BAYWBZ32.js";
|
|
29
29
|
|
|
30
30
|
// src/commands/boundary.ts
|
|
31
31
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -112,6 +112,7 @@ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
|
112
112
|
import nodePath5 from "path";
|
|
113
113
|
|
|
114
114
|
// templates/hooks/lib/dependency-readiness.ts
|
|
115
|
+
import { spawnSync } from "child_process";
|
|
115
116
|
import { createHash } from "crypto";
|
|
116
117
|
import {
|
|
117
118
|
existsSync as existsSync2,
|
|
@@ -119,6 +120,7 @@ import {
|
|
|
119
120
|
readdirSync,
|
|
120
121
|
readFileSync as readFileSync2,
|
|
121
122
|
realpathSync,
|
|
123
|
+
rmSync,
|
|
122
124
|
statSync as statSync2,
|
|
123
125
|
writeFileSync
|
|
124
126
|
} from "fs";
|
|
@@ -132,7 +134,7 @@ import nodePath from "path";
|
|
|
132
134
|
import nodePath2 from "path";
|
|
133
135
|
|
|
134
136
|
// templates/hooks/lib/test-runner.ts
|
|
135
|
-
import { execSync, spawnSync } from "child_process";
|
|
137
|
+
import { execSync, spawnSync as spawnSync2 } from "child_process";
|
|
136
138
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
137
139
|
import nodePath4 from "path";
|
|
138
140
|
var BDD_TEST_TIMEOUT_MS = 5 * 6e4;
|
|
@@ -628,4 +630,4 @@ function boundary(options) {
|
|
|
628
630
|
export {
|
|
629
631
|
boundary
|
|
630
632
|
};
|
|
631
|
-
//# sourceMappingURL=boundary-
|
|
633
|
+
//# sourceMappingURL=boundary-U5U2PSX2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/boundary.ts","../templates/hooks/lib/ledger-git.ts","../templates/hooks/lib/done-gate.ts","../templates/hooks/lib/dependency-readiness.ts","../templates/hooks/lib/namespace-root.ts","../templates/hooks/lib/shell-segments.ts","../templates/hooks/lib/test-runner.ts","../templates/hooks/lib/ledger-validation.ts","../src/boundary/engine.ts"],"sourcesContent":["/**\n * `safeword boundary --at commit|push` — reconcile workflow evidence at a git\n * boundary (ticket CDRJTW, #810 slice 1).\n *\n * Warn-and-record, never blocks: exit code is 0 on every path in this slice.\n * Findings print as warnings and append to `.safeword/boundary-audit.jsonl`\n * (gitignored — context for humans and retro, never trusted as evidence; the\n * server-side child of #810 re-derives from committed artifacts instead).\n * Changes touching no ticket artifacts are silent no-ops, keeping the\n * guardrails-never-tax-ordinary-commits promise.\n */\n\nimport { execFileSync } from 'node:child_process';\nimport { appendFileSync, existsSync, mkdirSync } from 'node:fs';\nimport nodePath from 'node:path';\nimport process from 'node:process';\n\nimport { createLedgerShaResolver } from '../../templates/hooks/lib/ledger-git.js';\nimport {\n type ChangedArtifact,\n findings,\n type LegalityStep,\n reconcileChange,\n TICKET_ARTIFACTS,\n type TicketChange,\n} from '../boundary/engine.js';\nimport {\n readConfiguredPathFromContent,\n ticketDirectoriesForConfiguredRoot,\n} from '../utils/configured-paths.js';\nimport { createPhaseAnchorScope } from '../utils/feature-source.js';\nimport { readFileSafe } from '../utils/fs.js';\nimport { warn } from '../utils/output.js';\n\nexport interface BoundaryOptions {\n at: string;\n}\n\ntype Boundary = 'commit' | 'push';\n\nconst AUDIT_RELATIVE_PATH = nodePath.join('.safeword', 'boundary-audit.jsonl');\nconst CONFIG_REPO_PATH = '.safeword/config.json';\n\n/** Run git, returning stdout or undefined on any failure (never throws). */\nfunction tryGit(cwd: string, args: string[]): string | undefined {\n try {\n return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });\n } catch {\n return undefined;\n }\n}\n\ninterface BoundaryRange {\n paths: string[];\n /** Revision whose file contents are \"prior\"; undefined = no prior (first push of everything). */\n priorRef?: string;\n}\n\nfunction splitLines(listing: string | undefined): string[] | undefined {\n return listing?.split('\\n').filter(line => line !== '');\n}\n\n/**\n * The outgoing range for a push: the configured upstream when set, else the\n * commits unreachable from every remote ref (a branch pushed for the first\n * time still gets its outgoing work reconciled — SM1.AC2).\n */\nfunction pushRange(cwd: string): BoundaryRange | undefined {\n const upstreamPaths = splitLines(tryGit(cwd, ['diff', '--name-only', '@{u}...HEAD']));\n if (upstreamPaths !== undefined) {\n // Prior content must come from the merge base, not the upstream tip: on a\n // diverged upstream, `@{u}:path` can hold content this branch never had,\n // and endpoint checks would compare against it. The three-dot path list\n // above is already merge-base-relative.\n const base = tryGit(cwd, ['merge-base', '@{u}', 'HEAD'])?.trim();\n return { paths: upstreamPaths, priorRef: base ?? '@{u}' };\n }\n\n const unpushed = splitLines(tryGit(cwd, ['rev-list', 'HEAD', '--not', '--remotes']));\n if (unpushed === undefined || unpushed.length === 0) return undefined;\n const oldest = unpushed.at(-1) ?? '';\n const base = tryGit(cwd, ['rev-parse', `${oldest}~1`])?.trim();\n const listing = tryGit(\n cwd,\n base === undefined\n ? ['log', '--name-only', '--format=', 'HEAD']\n : ['diff', '--name-only', `${base}..HEAD`],\n );\n const paths = splitLines(listing);\n return paths === undefined ? undefined : { paths: [...new Set(paths)], priorRef: base };\n}\n\n/** The change set at the boundary; undefined when git can't say (→ silent no-op). */\nfunction boundaryRange(cwd: string, at: Boundary): BoundaryRange | undefined {\n if (at === 'push') return pushRange(cwd);\n const staged = splitLines(\n tryGit(cwd, ['diff', '--cached', '--name-only', '--diff-filter=ACMRD']),\n );\n return staged === undefined ? undefined : { paths: staged, priorRef: 'HEAD' };\n}\n\n/** Content of a path at a git revision spec (e.g. `HEAD:p`, `:p`), or undefined. */\nfunction contentAt(cwd: string, spec: string): string | undefined {\n return tryGit(cwd, ['show', spec]);\n}\n\n/** One changed artifact's prior (range base) and proposed (index/HEAD) content. */\nfunction readChangedArtifact(\n cwd: string,\n path: string,\n basename: string,\n range: BoundaryRange,\n at: Boundary,\n): ChangedArtifact {\n const proposedSpec = at === 'commit' ? `:${path}` : `HEAD:${path}`;\n return {\n artifact: basename,\n prior: range.priorRef === undefined ? undefined : contentAt(cwd, `${range.priorRef}:${path}`),\n proposed: contentAt(cwd, proposedSpec),\n };\n}\n\n/** Split a repo-relative changed path into ticket folder + artifact basename. */\nfunction parseTicketPath(\n path: string,\n prefix: string,\n): { ticketFolder: string; basename: string } | undefined {\n if (!path.startsWith(prefix)) return undefined;\n const segments = path.slice(prefix.length).split('/');\n const ticketFolder = segments[0] ?? '';\n const basename = segments.slice(1).join('/');\n if (ticketFolder === '' || !TICKET_ARTIFACTS.has(basename)) return undefined;\n return { ticketFolder, basename };\n}\n\nfunction findChangedTicket(\n path: string,\n ticketsDirectories: string[],\n): { ticketFolder: string; basename: string; ticketsDirectory: string } | undefined {\n for (const ticketsDirectory of ticketsDirectories) {\n const parsed = parseTicketPath(path, `${ticketsDirectory}/`);\n if (parsed !== undefined) return { ...parsed, ticketsDirectory };\n }\n return undefined;\n}\n\n/** Map changed paths to per-ticket changes with prior/proposed + at-rest context. */\nfunction collectChanges(\n cwd: string,\n range: BoundaryRange,\n at: Boundary,\n ticketsDirectories: string[],\n configuredFeatures?: string,\n): TicketChange[] {\n const byTicket = new Map<string, TicketChange>();\n\n for (const path of range.paths) {\n const parsed = findChangedTicket(path, ticketsDirectories);\n if (parsed === undefined) continue;\n const ticketPath = `${parsed.ticketsDirectory}/${parsed.ticketFolder}`;\n const change = byTicket.get(ticketPath) ?? {\n anchorScope: createPhaseAnchorScope(cwd, ticketPath, configuredFeatures),\n artifacts: [],\n hasLedger: false,\n };\n change.artifacts.push(readChangedArtifact(cwd, path, parsed.basename, range, at));\n byTicket.set(ticketPath, change);\n }\n\n // At-rest context per touched ticket: ticket.md as it stands after the\n // change (staged version wins over disk) and whether a ledger exists.\n for (const change of byTicket.values()) {\n const folder = nodePath.join(cwd, change.anchorScope.ticketPath);\n const staged = change.artifacts.find(a => a.artifact === 'ticket.md')?.proposed;\n change.ticketCurrent = staged ?? readFileSafe(nodePath.join(folder, 'ticket.md'));\n change.hasLedger =\n change.artifacts.some(\n a => a.artifact === 'test-definitions.md' && a.proposed !== undefined,\n ) || existsSync(nodePath.join(folder, 'test-definitions.md'));\n // Push tier: legality is judged per commit in the range, never by its\n // endpoints (N76NQ0 — a range that traversed phases one legal step at a\n // time must not read as a skip).\n if (at === 'push' && change.artifacts.some(a => a.artifact === 'ticket.md')) {\n const path = `${change.anchorScope.ticketPath}/ticket.md`;\n change.legalitySteps = legalityStepsFor(cwd, path, range.priorRef);\n }\n }\n\n return byTicket.values().toArray();\n}\n\n/**\n * Per-commit ticket.md transitions across the outgoing range (oldest first):\n * each commit that touched the path, viewed against its own parent. An\n * unknown prior ref (first push of a whole history) walks every commit.\n */\nfunction legalityStepsFor(cwd: string, path: string, priorReference?: string): LegalityStep[] {\n const range = priorReference === undefined ? 'HEAD' : `${priorReference}..HEAD`;\n const commits = splitLines(tryGit(cwd, ['rev-list', '--reverse', range, '--', path])) ?? [];\n return commits.map(commit => ({\n prior: contentAt(cwd, `${commit}~1:${path}`),\n proposed: contentAt(cwd, `${commit}:${path}`) ?? '',\n commit: commit.slice(0, 7),\n }));\n}\n\nfunction appendAudit(cwd: string, entry: object): void {\n const auditPath = nodePath.join(cwd, AUDIT_RELATIVE_PATH);\n mkdirSync(nodePath.dirname(auditPath), { recursive: true });\n appendFileSync(auditPath, `${JSON.stringify(entry)}\\n`);\n}\n\nfunction treeContainsPath(cwd: string, at: Boundary, repoRelativeRoot: string): boolean {\n const args =\n at === 'commit'\n ? ['ls-files', '--cached', '--', `${repoRelativeRoot}/`]\n : ['ls-tree', '-r', '--name-only', 'HEAD', '--', `${repoRelativeRoot}/`];\n const listing = tryGit(cwd, args);\n return (listing?.trim().length ?? 0) > 0;\n}\n\nfunction resolveBoundaryTicketDirectories(\n cwd: string,\n at: Boundary,\n configuredProjectRoot?: string,\n): string[] {\n const directories = ticketDirectoriesForConfiguredRoot(cwd, configuredProjectRoot, root =>\n treeContainsPath(cwd, at, root),\n );\n if (configuredProjectRoot !== undefined && directories.length === 0) {\n warn(`Configured project root \"${configuredProjectRoot}\" is outside the repository.`);\n }\n return directories;\n}\n\n/** The reconciliation body — separated so the entry point stays a trivial guard. */\nfunction reconcileBoundary(cwd: string, at: Boundary): void {\n const range = boundaryRange(cwd, at);\n if (range === undefined || range.paths.length === 0) return;\n\n // The push tier verifies LEDGER SHAs against real history; the commit tier\n // stays content-only for SHAs (sub-second budget — no history walks).\n // Anchors are tree-only at both tiers: the reader serves the exact tree the\n // boundary ships — the staged index at commit, HEAD at push — never the\n // worktree and never git history (shallow clones and squashed branches\n // verify identically to full clones).\n const resolveSha = at === 'push' ? createLedgerShaResolver(cwd) : undefined;\n const readTreeArtifact = (relpath: string): string | undefined =>\n contentAt(cwd, at === 'commit' ? `:${relpath}` : `HEAD:${relpath}`);\n const configContent = readTreeArtifact(CONFIG_REPO_PATH);\n const configuredFeatures = readConfiguredPathFromContent(configContent, 'features');\n const configuredProjectRoot = readConfiguredPathFromContent(configContent, 'projectRoot');\n const ticketsDirectories = resolveBoundaryTicketDirectories(cwd, at, configuredProjectRoot);\n const changes = collectChanges(cwd, range, at, ticketsDirectories, configuredFeatures);\n if (changes.length === 0) return;\n\n const reconciliations = reconcileChange(changes, resolveSha, readTreeArtifact);\n for (const finding of findings(reconciliations)) {\n warn(\n `boundary(${at}) ${finding.ticket}: [${finding.check.check}] ${finding.check.detail ?? finding.check.verdict}`,\n );\n }\n\n appendAudit(cwd, {\n boundary: at,\n timestamp: new Date().toISOString(),\n head: tryGit(cwd, ['rev-parse', '--short', 'HEAD'])?.trim() ?? 'unknown',\n tickets: reconciliations,\n });\n}\n\n/**\n * Reconcile the boundary. Exit 0 on every path — including internal failure:\n * a boundary gate that blocks by accident violates the slice's contract, so\n * errors degrade to a warning, never a throw.\n */\nexport function boundary(options: BoundaryOptions): Promise<void> {\n try {\n const at: Boundary = options.at === 'push' ? 'push' : 'commit';\n const cwd = process.cwd();\n if (existsSync(nodePath.join(cwd, '.safeword'))) {\n reconcileBoundary(cwd, at);\n }\n } catch (error: unknown) {\n // Exit-0 is absolute in this slice: record the breakage, never block.\n warn(\n `boundary check could not complete: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n return Promise.resolve();\n}\n","// Git-backed SHA resolver for the done-gate annotation ledger.\n//\n// Rebase/amend rewrite commit IDs while preserving the patch. The ledger keeps\n// the old SHA annotation, so the done gate accepts it when the same patch is\n// reachable from HEAD under exactly one new commit ID.\n\nimport { execFileSync } from 'node:child_process';\n\nexport type LedgerShaResolution = string | false;\nexport type LedgerShaResolver = (sha: string) => LedgerShaResolution;\n\nfunction git(cwd: string, args: string[], input?: string): string {\n return execFileSync('git', args, {\n cwd,\n input,\n encoding: 'utf8',\n stdio: input === undefined ? ['ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'],\n }).trim();\n}\n\nfunction resolveCommitSha(cwd: string, sha: string): string | undefined {\n try {\n return git(cwd, ['rev-parse', '--verify', `${sha}^{commit}`]);\n } catch {\n return undefined;\n }\n}\n\nfunction isAncestorOfHead(cwd: string, sha: string): boolean {\n try {\n execFileSync('git', ['merge-base', '--is-ancestor', sha, 'HEAD'], {\n cwd,\n stdio: 'pipe',\n });\n return true;\n } catch {\n return false;\n }\n}\n\nfunction patchIdForCommit(cwd: string, sha: string): string | undefined {\n try {\n const patch = git(cwd, ['show', '--format=format:', '--patch', '--no-ext-diff', sha]);\n const output = git(cwd, ['patch-id', '--stable'], patch);\n return output.split(/\\s+/)[0] || undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction allReachableCommits(cwd: string): string[] {\n try {\n const output = git(cwd, ['rev-list', '--no-merges', 'HEAD']);\n return output === '' ? [] : output.split('\\n').filter(Boolean);\n } catch {\n return [];\n }\n}\n\nfunction buildReachablePatchMap(cwd: string): Map<string, string[]> {\n const patchIdToCommits = new Map<string, string[]>();\n for (const commit of allReachableCommits(cwd)) {\n const patchId = patchIdForCommit(cwd, commit);\n if (!patchId) continue;\n const commits = patchIdToCommits.get(patchId) ?? [];\n commits.push(commit);\n patchIdToCommits.set(patchId, commits);\n }\n return patchIdToCommits;\n}\n\nexport function createLedgerShaResolver(cwd: string): LedgerShaResolver {\n let reachablePatchMap: Map<string, string[]> | undefined;\n\n function patchMap(): Map<string, string[]> {\n reachablePatchMap ??= buildReachablePatchMap(cwd);\n return reachablePatchMap;\n }\n\n return (sha: string): LedgerShaResolution => {\n const fullSha = resolveCommitSha(cwd, sha);\n if (!fullSha) return false;\n if (isAncestorOfHead(cwd, fullSha)) return fullSha;\n\n const stalePatchId = patchIdForCommit(cwd, fullSha);\n if (!stalePatchId) return false;\n\n const candidates = patchMap().get(stalePatchId) ?? [];\n const [candidate] = candidates;\n return candidates.length === 1 && candidate ? candidate : false;\n };\n}\n","// Safeword: shared done-gate evidence checks.\n//\n// The done gate is the \"you may close this ticket\" chokepoint. On Claude Code it\n// runs inside the blocking Stop hook (stop-quality.ts). Cursor's `stop` cannot\n// block, so the same enforcement is applied one layer earlier — at the edit that\n// flips `ticket.md` to `status: done` (the Cursor preToolUse adapter). Both paths\n// MUST agree on what counts as evidence, so the shared logic lives here:\n//\n// - `checkVerifyArtifact` is the exact PR-scope check the Stop hook uses (it\n// imports this function — there is no second copy to drift).\n// - `evaluateDoneEvidence` is the composite the Cursor edit gate calls. It runs\n// the same dependency -> tests -> verify.md -> scenarios sequence the Stop\n// hook's dependency/test/verify/scenario subset, returning a plain verdict\n// instead of exiting. Claude still has transcript-only checks Cursor cannot\n// run from preToolUse, so the two gates intentionally diverge there.\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { formatDependencyRecovery, getDependencyReadiness } from './dependency-readiness.js';\nimport { analyzeScenarioFormat } from './scenario-format.js';\nimport { runTests } from './test-runner.js';\n\n/** A single line of verify.md that records whether the closing diff stayed in scope. */\nconst PR_SCOPE_LINE_PATTERN = /^\\*\\*PR Scope:\\*\\*\\s*(?<status>.+)$/im;\n\nexport interface VerifyArtifactStatus {\n ok: boolean;\n reason?: string;\n}\n\n/**\n * Validate the PR-scope evidence inside verify.md. The /verify skill records a\n * `**PR Scope:**` line stating whether the closing diff matched the ticket; a\n * missing line or a failed/❌ status blocks done so unrelated work can't ride\n * along in the closing change. Shared verbatim with the Claude Stop gate.\n */\nexport function checkVerifyArtifact(content: string): VerifyArtifactStatus {\n const prScopeMatch = PR_SCOPE_LINE_PATTERN.exec(content);\n if (!prScopeMatch?.groups?.status) {\n return {\n ok: false,\n reason:\n 'verify.md is missing PR scope evidence. Run /verify again so it records `**PR Scope:**` before marking done.',\n };\n }\n\n // Status semantics (1F08DD): the glyph decides. ❌ anywhere fails (it may be\n // followed by explanatory prose, even a ✅ quote); otherwise an explicit ✅\n // passes REGARDLESS of prose — \"No piggybacked changes.\" on a passing line\n // must not read as a failure (found live when the boundary gate rejected an\n // honest verify.md). The bare-substring test survives only as the fallback\n // for glyph-less legacy lines, where \"piggybacked changes\" is a positive claim.\n const status = prScopeMatch.groups.status;\n const failed =\n status.includes('❌') || (!status.includes('✅') && /piggybacked changes/i.test(status));\n if (failed) {\n return {\n ok: false,\n reason:\n 'verify.md says PR scope failed. Revert or split the piggybacked work, or explicitly accept the scope change in the ticket artifacts before marking done.',\n };\n }\n\n return { ok: true };\n}\n\nexport interface DoneEvidenceParams {\n /** Project root — where dependencies and the test suite live. */\n projectDir: string;\n /** Absolute path to the ticket folder being closed (holds verify.md, test-definitions.md). */\n ticketDir: string;\n /** The ticket's `type` frontmatter ('feature' | 'task' | ...); features also need scenarios. */\n ticketType: string | undefined;\n}\n\nexport interface DoneEvidenceVerdict {\n /** True when every applicable check passed and the ticket may be closed. */\n ok: boolean;\n /** Human-readable explanation of the first failing check (undefined when ok). */\n reason?: string;\n}\n\n/**\n * Evaluate whether a ticket has the evidence required to be marked done.\n *\n * Mirrors the Claude Stop gate's dependency/test/verify/scenario subset — deps\n * ready, tests green, verify.md present and in-scope, and (for features) all\n * scenarios complete — but returns a verdict instead of blocking, so the Cursor\n * edit gate can translate it into a `deny` decision. Tests run here (\"full\"\n * enforcement for this subset, ticket AKNWZK): the suite is the one piece of\n * evidence prose can't fake.\n *\n * Cursor divergence: where the Claude gate falls back to scanning the agent's last\n * message for \"X/X tests pass\" on a task with no test command, this requires a\n * valid verify.md instead — Cursor's preToolUse payload has no transcript to scan,\n * and verify.md is the stronger artifact anyway.\n */\nexport function evaluateDoneEvidence(params: DoneEvidenceParams): DoneEvidenceVerdict {\n const { projectDir, ticketDir, ticketType } = params;\n\n // 1. Dependencies must be installed, or a missing toolchain masquerades as a\n // test failure (the runner exits 127). A verification gate that cannot run\n // its check must not pass — fail closed with the install recovery.\n const readiness = getDependencyReadiness(projectDir);\n if (readiness.status === 'missing' || readiness.status === 'stale') {\n return { ok: false, reason: formatDependencyRecovery(readiness) };\n }\n\n // 2. Tests are the authoritative external gate — they can't be gamed by prose.\n // skipped=true means no test command exists; verify.md (step 3) carries the\n // evidence in that case.\n const testResult = runTests(projectDir);\n if (!testResult.skipped && !testResult.passed) {\n if (testResult.toolchainMissing) {\n return {\n ok: false,\n reason: `Test toolchain not found — dependencies are likely not installed. Install them, then retry.\\n\\n${testResult.output}`,\n };\n }\n return {\n ok: false,\n reason: `Tests failed. Fix failures before marking done.\\n\\n${testResult.output}`,\n };\n }\n\n // 3. verify.md — written by /verify when all checks pass, including the PR-scope\n // evidence that keeps unrelated work out of the closing change.\n const verifyPath = nodePath.join(ticketDir, 'verify.md');\n const verifyContent = existsSync(verifyPath) ? readFileSync(verifyPath, 'utf8') : '';\n if (verifyContent.trim().length === 0) {\n return {\n ok: false,\n reason:\n 'No valid verify.md found in the ticket folder. Run /verify to generate evidence before marking done.',\n };\n }\n const verifyStatus = checkVerifyArtifact(verifyContent);\n if (!verifyStatus.ok) {\n return { ok: false, reason: verifyStatus.reason };\n }\n\n // 4. Features must have every scenario checked off in test-definitions.md.\n if (ticketType === 'feature') {\n const scenarioVerdict = checkFeatureScenarios(ticketDir);\n if (!scenarioVerdict.ok) return scenarioVerdict;\n }\n\n return { ok: true };\n}\n\n/** Require test-definitions.md to exist and have all GFM scenario checkboxes ticked. */\nfunction checkFeatureScenarios(ticketDir: string): DoneEvidenceVerdict {\n const testDefsPath = nodePath.join(ticketDir, 'test-definitions.md');\n if (!existsSync(testDefsPath)) {\n return {\n ok: false,\n reason:\n 'Feature done requires test-definitions.md with at least one scenario. Create it before marking done.',\n };\n }\n\n const { checked, unchecked, isUnrecognized } = analyzeScenarioFormat(\n readFileSync(testDefsPath, 'utf8'),\n );\n if (isUnrecognized) {\n return {\n ok: false,\n reason:\n 'test-definitions.md has content but no GFM checkboxes (- [ ] / - [x]). Convert scenarios to GFM task-list items.',\n };\n }\n const total = checked + unchecked;\n if (total === 0 || unchecked > 0) {\n return {\n ok: false,\n reason:\n 'Not all scenarios are complete in test-definitions.md. Mark every scenario checkbox [x] before marking done.',\n };\n }\n\n return { ok: true };\n}\n","import { spawnSync } from 'node:child_process';\nimport { createHash } from 'node:crypto';\nimport {\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n realpathSync,\n rmSync,\n statSync,\n writeFileSync,\n type Dirent,\n} from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { resolveNamespaceRoot } from './namespace-root.js';\nimport { commandWords, parseShellCommandList, splitShellSegments } from './shell-segments.js';\n\nexport type DependencyManager = 'bun' | 'pnpm' | 'npm' | 'yarn';\nexport type DependencyReadinessStatus = 'ready' | 'missing' | 'stale' | 'unsupported';\n\nexport interface InstallCommand {\n binary: string;\n args: string[];\n display: string;\n}\n\nexport interface DependencyPlan {\n manager: DependencyManager;\n installCommand: InstallCommand;\n installArtifact: string;\n inputPaths: string[];\n}\n\nexport interface DependencyReadiness {\n status: DependencyReadinessStatus;\n reason:\n | 'install_artifact_current'\n | 'install_artifact_missing'\n | 'install_artifact_stale'\n | 'no_supported_package_manager';\n installCommand?: string;\n fingerprint?: string;\n plan?: DependencyPlan;\n}\n\nexport interface DependencyBootstrapConfig {\n autoInstall: boolean;\n}\n\nexport interface DependencyReadinessState {\n status: DependencyReadinessStatus | 'installing' | 'failed';\n reason?: string;\n fingerprint?: string;\n installCommand?: string;\n message?: string;\n updatedAt: string;\n}\n\nexport type DependencyBootstrapResult =\n | { status: 'ready' | 'unsupported' }\n | { status: 'bootstrapped'; message: string }\n | { status: 'action_required' | 'failed'; message: string };\n\nconst INSTALL_ARTIFACT = 'node_modules';\nconst INSTALL_MARKER_FILENAME = '.safeword-deps-fingerprint';\nconst DEPENDENCY_STATE_FILENAME = 'dependency-readiness.json';\nconst DEPENDENCY_BOOTSTRAP_LOCK_DIRECTORY = '.dependency-bootstrap.lock';\nconst BUN_LOCKFILES = ['bun.lock', 'bun.lockb'];\nconst WORKSPACE_SCAN_EXCLUDED_DIRECTORIES = new Set([\n '.git',\n '.project',\n '.safeword',\n '.safeword-project',\n 'node_modules',\n]);\nconst BUN_OPTIONS_WITH_VALUES = new Set([\n '--config',\n '--conditions',\n '--cwd',\n '--env-file',\n '--import',\n '--install',\n '--preload',\n '--require',\n '-c',\n '-r',\n]);\nconst PACKAGE_MANAGER_OPTIONS_WITH_VALUES = new Set([\n '--cwd',\n '--dir',\n '--filter',\n '--prefix',\n '--workspace',\n '-C',\n '-F',\n '-w',\n]);\nconst PACKAGE_SCRIPT_COMMANDS = new Set(['run', 'test']);\nconst BUNX_BOOLEAN_OPTIONS = new Set(['--bun', '--no-install', '--silent', '--verbose']);\nconst SAFEWORD_GLOBAL_BOOLEAN_OPTIONS = new Set([\n '--json',\n '--no-input',\n '--offline',\n '--quiet',\n '--verbose',\n '--version',\n '-V',\n '-v',\n]);\nconst SAFEWORD_GLOBAL_OPTIONS_WITH_VALUES = new Set(['--cwd']);\nconst SAFEWORD_RECOVERY_COMMANDS = new Set(['doctor', 'plan', 'setup', 'status']);\nconst DEPENDENCY_BINARIES = new Set([\n 'cypress',\n 'dependency-cruiser',\n 'depcruise',\n 'eslint',\n 'gherkin-lint',\n 'jest',\n 'jscpd',\n 'next',\n 'playwright',\n 'prettier',\n 'tsc',\n 'tsup',\n 'tsx',\n 'turbo',\n 'vite',\n 'vitest',\n]);\n\nconst MANAGER_LOCKFILES: Record<DependencyManager, readonly string[]> = {\n bun: BUN_LOCKFILES,\n pnpm: ['pnpm-lock.yaml'],\n npm: ['package-lock.json'],\n yarn: ['yarn.lock'],\n};\n\n// Lockfile-only precedence when nothing declares a manager — mirrors install.ts:\n// a bun lockfile beats pnpm-lock.yaml, then yarn.lock, then package-lock.json.\nconst LOCKFILE_PRECEDENCE: readonly DependencyManager[] = ['bun', 'pnpm', 'yarn', 'npm'];\n\nexport function detectDependencyPlan(projectDirectory: string): DependencyPlan | undefined {\n const packageJson = readJsonFile<Record<string, unknown>>(\n nodePath.join(projectDirectory, 'package.json'),\n );\n if (packageJson === undefined) return undefined;\n\n const packageManager =\n typeof packageJson.packageManager === 'string' ? packageJson.packageManager : undefined;\n\n switch (detectDependencyManager(projectDirectory, packageManager)) {\n case 'bun':\n return buildBunPlan(projectDirectory, packageJson);\n case 'pnpm':\n return buildPnpmPlan(projectDirectory);\n case 'npm':\n return buildNpmPlan(projectDirectory, packageJson);\n case 'yarn':\n if (usesYarnPlugAndPlay(projectDirectory, packageManager)) return undefined;\n return buildYarnPlan(projectDirectory, packageJson, packageManager);\n default:\n return undefined;\n }\n}\n\n/**\n * Resolve the readiness-supported package manager (bun, pnpm, npm, or yarn), or\n * undefined when unsupported. An explicit `packageManager` declaration is\n * authoritative (honored when its lockfile exists); otherwise a pnpm workspace\n * forces pnpm (beats a coexisting bun lockfile — mirrors install.ts); otherwise\n * the manager is chosen by lockfile in install.ts precedence (bun > pnpm-lock >\n * yarn > npm). Every manager requires its lockfile to produce a plan, so a\n * declared-but-uninstalled manager (or a stray foreign lockfile in another\n * manager's project) abstains rather than misfiring (#321/#323/#327).\n */\nfunction detectDependencyManager(\n projectDirectory: string,\n packageManager: string | undefined,\n): DependencyManager | undefined {\n if (packageManager !== undefined && packageManager.trim().length > 0) {\n const declared = parseDeclaredManager(packageManager);\n return declared !== undefined && managerLockfilePresent(projectDirectory, declared)\n ? declared\n : undefined;\n }\n\n if (existsSync(nodePath.join(projectDirectory, 'pnpm-workspace.yaml'))) {\n return managerLockfilePresent(projectDirectory, 'pnpm') ? 'pnpm' : undefined;\n }\n\n return LOCKFILE_PRECEDENCE.find(manager => managerLockfilePresent(projectDirectory, manager));\n}\n\nfunction parseDeclaredManager(packageManager: string | undefined): DependencyManager | undefined {\n const match = packageManager?.match(/^(bun|pnpm|npm|yarn)@.+/);\n return match ? (match[1] as DependencyManager) : undefined;\n}\n\nfunction managerLockfilePresent(projectDirectory: string, manager: DependencyManager): boolean {\n return MANAGER_LOCKFILES[manager].some(lockfile =>\n existsSync(nodePath.join(projectDirectory, lockfile)),\n );\n}\n\nfunction buildBunPlan(\n projectDirectory: string,\n packageJson: Record<string, unknown>,\n): DependencyPlan {\n const bunLockfile =\n BUN_LOCKFILES.find(lockfile => existsSync(nodePath.join(projectDirectory, lockfile))) ??\n 'bun.lock';\n return {\n manager: 'bun',\n installCommand: { binary: 'bun', args: ['ci'], display: 'bun ci' },\n installArtifact: INSTALL_ARTIFACT,\n inputPaths: uniqueSorted([\n 'package.json',\n bunLockfile,\n ...collectWorkspacePackageJsonPaths(projectDirectory, packageJson),\n ]),\n };\n}\n\n/**\n * pnpm readiness plan: `pnpm install --frozen-lockfile` (the frozen analog of\n * `bun ci`), fingerprinting package.json, the pnpm lockfile, the workspace\n * config, and the workspace package manifests it globs in.\n */\nfunction buildPnpmPlan(projectDirectory: string): DependencyPlan {\n const workspaceConfigPresent = existsSync(nodePath.join(projectDirectory, 'pnpm-workspace.yaml'));\n return {\n manager: 'pnpm',\n installCommand: {\n binary: 'pnpm',\n args: ['install', '--frozen-lockfile'],\n display: 'pnpm install --frozen-lockfile',\n },\n installArtifact: INSTALL_ARTIFACT,\n inputPaths: uniqueSorted([\n 'package.json',\n 'pnpm-lock.yaml',\n ...(workspaceConfigPresent ? ['pnpm-workspace.yaml'] : []),\n ...collectPnpmWorkspacePackageJsonPaths(projectDirectory),\n ]),\n };\n}\n\nfunction buildNpmPlan(\n projectDirectory: string,\n packageJson: Record<string, unknown>,\n): DependencyPlan {\n return {\n manager: 'npm',\n installCommand: { binary: 'npm', args: ['ci'], display: 'npm ci' },\n installArtifact: INSTALL_ARTIFACT,\n inputPaths: uniqueSorted([\n 'package.json',\n 'package-lock.json',\n ...collectWorkspacePackageJsonPaths(projectDirectory, packageJson),\n ]),\n };\n}\n\n/**\n * yarn readiness plan. Classic (v1) uses `--frozen-lockfile`; Berry (v2+) uses\n * `--immutable` (its rename of the same CI guard). Berry is supported only\n * with an explicit node-modules linker because Plug'n'Play has no node_modules\n * artifact for this readiness contract to validate.\n */\nfunction buildYarnPlan(\n projectDirectory: string,\n packageJson: Record<string, unknown>,\n packageManager: string | undefined,\n): DependencyPlan {\n const yarnBerry = isYarnBerry(projectDirectory, packageManager);\n const args = yarnBerry ? ['install', '--immutable'] : ['install', '--frozen-lockfile'];\n return {\n manager: 'yarn',\n installCommand: { binary: 'yarn', args, display: `yarn ${args.join(' ')}` },\n installArtifact: INSTALL_ARTIFACT,\n inputPaths: uniqueSorted([\n 'package.json',\n 'yarn.lock',\n ...(yarnBerry ? ['.yarnrc.yml'] : []),\n ...collectWorkspacePackageJsonPaths(projectDirectory, packageJson),\n ]),\n };\n}\n\nfunction isYarnBerry(projectDirectory: string, packageManager: string | undefined): boolean {\n if (packageManager?.startsWith('yarn@')) {\n const major = Number.parseInt(packageManager.slice('yarn@'.length), 10);\n return Number.isFinite(major) && major >= 2;\n }\n return existsSync(nodePath.join(projectDirectory, '.yarnrc.yml'));\n}\n\nfunction usesYarnPlugAndPlay(\n projectDirectory: string,\n packageManager: string | undefined,\n): boolean {\n if (!isYarnBerry(projectDirectory, packageManager)) return false;\n\n try {\n const yarnConfig = readFileSync(nodePath.join(projectDirectory, '.yarnrc.yml'), 'utf8');\n return !/^\\s*nodeLinker\\s*:\\s*(['\"]?)node-modules\\1\\s*(?:#.*)?$/mu.test(yarnConfig);\n } catch {\n // Yarn Berry defaults to Plug'n'Play when nodeLinker is not configured.\n return true;\n }\n}\n\nexport function dependencyInputFingerprint(projectDirectory: string, plan: DependencyPlan): string {\n const hash = createHash('sha256');\n\n for (const inputPath of plan.inputPaths.toSorted()) {\n hash.update(inputPath);\n hash.update('\\0');\n const inputFilePath = nodePath.resolve(projectDirectory, inputPath);\n if (!isProjectPathContained(projectDirectory, inputFilePath)) {\n hash.update('<outside-project>');\n hash.update('\\0');\n continue;\n }\n try {\n hash.update(readFileSync(inputFilePath));\n } catch {\n hash.update('<missing>');\n }\n hash.update('\\0');\n }\n\n return hash.digest('hex');\n}\n\nexport function getDependencyReadiness(projectDirectory: string): DependencyReadiness {\n const plan = detectDependencyPlan(projectDirectory);\n if (plan === undefined) {\n return {\n status: 'unsupported',\n reason: 'no_supported_package_manager',\n };\n }\n\n const fingerprint = dependencyInputFingerprint(projectDirectory, plan);\n const installCommand = plan.installCommand.display;\n const artifactPath = nodePath.join(projectDirectory, plan.installArtifact);\n const previousState = readDependencyReadinessState(projectDirectory);\n\n // An installer may delete and partially recreate node_modules before it is\n // interrupted. Preserve the pre-install classification in durable state so\n // a partial tree can never be mistaken for ready merely because it is new.\n if (\n (previousState?.status === 'installing' || previousState?.status === 'failed') &&\n previousState.fingerprint === fingerprint &&\n (previousState.reason === 'install_artifact_missing' ||\n previousState.reason === 'install_artifact_stale')\n ) {\n return {\n status: previousState.reason === 'install_artifact_missing' ? 'missing' : 'stale',\n reason: previousState.reason,\n installCommand,\n fingerprint,\n plan,\n };\n }\n\n if (!isDirectory(artifactPath)) {\n return {\n status: 'missing',\n reason: 'install_artifact_missing',\n installCommand,\n fingerprint,\n plan,\n };\n }\n\n // The content-fingerprint marker is the authoritative freshness signal: it\n // survives content-preserving operations (rebase, checkout, clone, cp) that\n // bump input mtimes without changing input content. mtime is only a bootstrap\n // fallback for the first check after an install, before any hook has stamped\n // the marker. Once present, a mismatched marker is authoritative: a newer\n // artifact mtime cannot prove that its contents match the dependency inputs.\n const marker = readInstallMarker(projectDirectory, plan);\n const markerMismatch = marker !== undefined && marker !== fingerprint;\n\n if (\n markerMismatch ||\n (marker === undefined && isInstallArtifactStale(projectDirectory, plan, artifactPath))\n ) {\n return {\n status: 'stale',\n reason: 'install_artifact_stale',\n installCommand,\n fingerprint,\n plan,\n };\n }\n\n return {\n status: 'ready',\n reason: 'install_artifact_current',\n installCommand,\n fingerprint,\n plan,\n };\n}\n\nexport function readDependencyBootstrapConfig(projectDirectory: string): DependencyBootstrapConfig {\n const configPath = nodePath.join(projectDirectory, '.safeword', 'config.json');\n const parsed = readJsonFile<{ dependencyBootstrap?: { autoInstall?: unknown } }>(configPath);\n\n return {\n autoInstall: parsed?.dependencyBootstrap?.autoInstall === true,\n };\n}\n\n/**\n * Whether a trusted project's SessionStart should auto-install dependencies for this readiness\n * status. A `missing` install artifact (no `node_modules` — e.g. a fresh git\n * worktree) is bootstrapped UNCONDITIONALLY: the worktree is unusable and a\n * commit would bypass the husky guard chain (lint-staged can't resolve its\n * tools), so install regardless of the `autoInstall` opt-in. The opt-in still\n * governs the softer `stale` re-install (deps present but inputs changed).\n * Claude and Codex both load repo-owned SessionStart hooks only after the user\n * trusts the project, so this never installs from an untrusted checkout. (JNVP4W)\n */\nexport function shouldBootstrapDependencies(\n status: DependencyReadinessStatus,\n autoInstall: boolean,\n): boolean {\n if (status === 'missing') return true;\n if (status === 'stale') return autoInstall;\n return false;\n}\n\n/**\n * Reconcile dependency readiness without assuming a host hook protocol.\n * Host adapters decide how to surface the typed result: Claude emits\n * SessionStart JSON, while Codex/local setup uses plain output and exit status.\n */\nexport function bootstrapDependencies(projectDirectory: string): DependencyBootstrapResult {\n let readiness = getDependencyReadiness(projectDirectory);\n\n if (readiness.status === 'unsupported') return { status: 'unsupported' };\n\n if (readiness.status === 'ready') {\n writeDependencyReadinessState(projectDirectory, toDependencyReadinessState(readiness));\n writeInstallMarker(projectDirectory, readiness);\n return { status: 'ready' };\n }\n\n const config = readDependencyBootstrapConfig(projectDirectory);\n if (\n shouldBootstrapDependencies(readiness.status, config.autoInstall) &&\n readiness.plan !== undefined\n ) {\n const releaseLock = acquireDependencyBootstrapLock(projectDirectory);\n if (releaseLock === undefined) {\n return {\n status: 'action_required',\n message:\n 'another dependency bootstrap is already running; wait for it to finish, then retry.',\n };\n }\n const initialReadiness = readiness;\n const plan = readiness.plan;\n const { binary, args, display } = plan.installCommand;\n try {\n writeDependencyReadinessState(projectDirectory, {\n status: 'installing',\n reason: initialReadiness.reason,\n fingerprint: initialReadiness.fingerprint,\n installCommand: initialReadiness.installCommand,\n });\n const result = spawnSync(binary, args, {\n cwd: projectDirectory,\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n if (\n result.status === 0 &&\n initialReadiness.fingerprint !== undefined &&\n isDirectory(nodePath.join(projectDirectory, plan.installArtifact))\n ) {\n stampInstallMarker(projectDirectory, plan, initialReadiness.fingerprint);\n readiness = getDependencyReadinessIgnoringInstallState(projectDirectory);\n }\n if (result.status === 0 && readiness.status === 'ready') {\n writeDependencyReadinessState(projectDirectory, toDependencyReadinessState(readiness));\n return {\n status: 'bootstrapped',\n message: `dependencies bootstrapped with \\`${display}\\`.`,\n };\n }\n\n const summary = [\n `dependency bootstrap failed while running \\`${display}\\`.`,\n 'Run the install command manually, inspect the package manager output, then retry.',\n ].join('\\n');\n const message = [\n summary,\n result.error?.message,\n trimBootstrapOutput(result.stderr) || trimBootstrapOutput(result.stdout),\n ]\n .filter(Boolean)\n .join('\\n');\n\n writeDependencyReadinessState(projectDirectory, {\n status: 'failed',\n reason: initialReadiness.reason,\n fingerprint: initialReadiness.fingerprint,\n installCommand: initialReadiness.installCommand,\n message: summary,\n });\n return { status: 'failed', message };\n } finally {\n releaseLock();\n }\n }\n\n writeDependencyReadinessState(projectDirectory, toDependencyReadinessState(readiness));\n return { status: 'action_required', message: formatDependencyRecovery(readiness) };\n}\n\nfunction trimBootstrapOutput(output: string | undefined): string {\n return output?.trim().split('\\n').slice(-20).join('\\n') ?? '';\n}\n\nexport function isDependencyBackedCommand(command: string): boolean {\n const segments = splitShellSegments(command);\n\n return segments.some(segment => isDependencyBackedSegment(segment));\n}\n\n/**\n * A stale-readiness recovery may run its retry in the same Bash call only when\n * the recovery comes first and every following segment depends on its success.\n * This preserves the pre-tool gate for `||`, `;`, and pipes, where a guarded\n * command could otherwise run after a failed or concurrent recovery (#1763).\n *\n * A background `&` also breaks that guarantee: Bash ends the preceding list\n * and runs it asynchronously. The shared tokenizer exposes it as its own\n * control operator, so it cannot look like an all-`&&` recovery chain.\n *\n * Only the LEADING segment is classified. An intermediate segment that undoes\n * the recovery (`bun ci && rm -rf node_modules && bun run test`) still passes:\n * the gate stops an agent from running into a stale worktree by accident, not\n * from dismantling its own recovery on purpose.\n */\nexport function isDependencyReadinessRecoveryCommand(\n command: string,\n status: DependencyReadinessStatus,\n): boolean {\n const segments = parseShellCommandList(command);\n const [first] = segments;\n if (segments.length < 2 || first === undefined || !isRecoverySegment(first.command, status)) {\n return false;\n }\n return segments.slice(0, -1).every(segment => segment.operatorAfter === '&&');\n}\n\n/** Package managers whose install/ci/i reconciles `node_modules` against the inputs. */\nconst INSTALL_MANAGERS = new Set(['bun', 'pnpm', 'npm', 'yarn']);\n/** Subcommands that perform a dependency install (not `add`/`remove`, which change inputs). */\nconst INSTALL_SUBCOMMANDS = new Set(['install', 'i', 'ci']);\n/**\n * Flags that prevent an install from fully reconciling project dependencies.\n * Some do not materialize `node_modules` at all; others omit dependencies or\n * skip linking. Treating either as ready would let a recovery retry run with an\n * incomplete tree and would let the post-tool hook stamp a sticky false-ready.\n */\nconst NON_RECONCILING_INSTALL_FLAGS = new Set([\n '--dry-run',\n '--lockfile-only',\n '--package-lock-only',\n '--production',\n '--prod',\n '-P',\n '--no-dev',\n '--no-optional',\n]);\nconst NON_RECONCILING_INSTALL_OPTIONS = new Set(['--omit', '--only', '--mode']);\n/**\n * Flags that make any package manager print-and-exit instead of installing.\n * `bun install --help`, `npm ci --version`, and bare `yarn --version` all\n * carry a real install subcommand (or are classic bare yarn) yet never\n * reconcile `node_modules`, so counting them as installs would stamp a\n * false-ready (EDDABK). Under-counting is safe here — the worst case is the\n * user re-running a real install — so a broad flag list is fine.\n */\nconst REPORT_ONLY_INSTALL_FLAGS = new Set(['--version', '-v', '--help', '-h']);\n\n/**\n * Whether a command runs a dependency *install* (e.g. `bun ci`, `pnpm install\n * --frozen-lockfile`, `npm ci`, bare `yarn`). A successful install reconciles\n * `node_modules` with the current inputs, so the post-tool hook can stamp the\n * fingerprint marker — making the recommended recovery command clear the\n * stale-readiness block even when the install is a mtime-preserving no-op (#380).\n */\nexport function isDependencyInstallCommand(command: string): boolean {\n const segments = parseShellCommandList(command);\n return segments.length === 1 && isInstallSegment(segments[0]?.command ?? '');\n}\n\nfunction isInstallSegment(segment: string): boolean {\n const [binary, ...args] = commandWords(segment);\n if (binary === undefined) return false;\n const base = nodePath.basename(binary);\n if (!INSTALL_MANAGERS.has(base)) return false;\n if (hasNonReconcilingInstallOption(args)) return false;\n // A report-only flag (--help/--version) makes the manager print and exit\n // without installing — for every manager, not just classic bare yarn.\n if (args.some(arg => REPORT_ONLY_INSTALL_FLAGS.has(arg))) return false;\n\n const subcommand = firstCommandArgument(args, PACKAGE_MANAGER_OPTIONS_WITH_VALUES);\n // Classic `yarn` with no subcommand installs.\n if (base === 'yarn' && subcommand === undefined) return true;\n return subcommand !== undefined && INSTALL_SUBCOMMANDS.has(subcommand);\n}\n\nfunction hasNonReconcilingInstallOption(args: string[]): boolean {\n return args.some(arg => {\n const [flag] = arg.split('=', 1);\n if (flag === undefined) return false;\n return NON_RECONCILING_INSTALL_FLAGS.has(flag) || NON_RECONCILING_INSTALL_OPTIONS.has(flag);\n });\n}\n\n/**\n * `touch node_modules` is only offered as recovery for a STALE marker, and it\n * only earns the exemption there. With `node_modules` missing, `touch` creates\n * an empty regular FILE of that name and exits 0 — so the retry would run with\n * nothing installed, readiness would stay `missing` forever (the path is not a\n * directory), and the stray file would block the real install that follows.\n */\nfunction isRecoverySegment(segment: string, status: DependencyReadinessStatus): boolean {\n if (isInstallSegment(segment)) return true;\n return status === 'stale' && isTouchNodeModulesSegment(segment);\n}\n\nfunction isTouchNodeModulesSegment(segment: string): boolean {\n const [binary, ...args] = commandWords(segment);\n return (\n nodePath.basename(binary ?? '') === 'touch' && args.length === 1 && args[0] === 'node_modules'\n );\n}\n\nexport function getDependencyReadinessStatePath(projectDirectory: string): string {\n return nodePath.join(resolveNamespaceRoot(projectDirectory), DEPENDENCY_STATE_FILENAME);\n}\n\nexport function writeDependencyReadinessState(\n projectDirectory: string,\n state: Omit<DependencyReadinessState, 'updatedAt'> & { updatedAt?: string },\n): void {\n try {\n const statePath = getDependencyReadinessStatePath(projectDirectory);\n mkdirSync(nodePath.dirname(statePath), { recursive: true });\n writeFileSync(\n statePath,\n JSON.stringify(\n {\n ...state,\n updatedAt: state.updatedAt ?? new Date().toISOString(),\n },\n null,\n 2,\n ),\n );\n } catch {\n // Hook state is best-effort. Readiness enforcement should not crash because\n // a namespace directory is unwritable or temporarily unavailable.\n }\n}\n\nexport function readDependencyReadinessState(\n projectDirectory: string,\n): DependencyReadinessState | undefined {\n return readJsonFile<DependencyReadinessState>(getDependencyReadinessStatePath(projectDirectory));\n}\n\nexport function writeInstallMarker(projectDirectory: string, readiness: DependencyReadiness): void {\n if (readiness.status !== 'ready') return;\n const { plan, fingerprint } = readiness;\n if (plan === undefined || fingerprint === undefined) return;\n\n stampInstallMarker(projectDirectory, plan, fingerprint);\n}\n\nfunction stampInstallMarker(\n projectDirectory: string,\n plan: DependencyPlan,\n fingerprint: string,\n): void {\n try {\n writeFileSync(installMarkerPath(projectDirectory, plan), fingerprint);\n } catch {\n // The marker shares node_modules' lifecycle and is best-effort. A failure\n // to stamp it simply falls back to the mtime check on the next read.\n }\n}\n\nfunction getDependencyReadinessIgnoringInstallState(projectDirectory: string): DependencyReadiness {\n rmSync(getDependencyReadinessStatePath(projectDirectory), { force: true });\n return getDependencyReadiness(projectDirectory);\n}\n\nfunction acquireDependencyBootstrapLock(projectDirectory: string): (() => void) | undefined {\n const lockPath = nodePath.join(\n resolveNamespaceRoot(projectDirectory),\n DEPENDENCY_BOOTSTRAP_LOCK_DIRECTORY,\n );\n mkdirSync(nodePath.dirname(lockPath), { recursive: true });\n for (let attempt = 0; attempt < 2; attempt += 1) {\n try {\n mkdirSync(lockPath);\n writeFileSync(nodePath.join(lockPath, 'pid'), String(process.pid));\n return () => rmSync(lockPath, { recursive: true, force: true });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') return undefined;\n const pid = Number.parseInt(readTextFile(nodePath.join(lockPath, 'pid')) ?? '', 10);\n if (Number.isFinite(pid) && processIsRunning(pid)) return undefined;\n rmSync(lockPath, { recursive: true, force: true });\n }\n }\n return undefined;\n}\n\nfunction processIsRunning(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction readTextFile(filePath: string): string | undefined {\n try {\n return readFileSync(filePath, 'utf8');\n } catch {\n return undefined;\n }\n}\n\nfunction readInstallMarker(projectDirectory: string, plan: DependencyPlan): string | undefined {\n try {\n return readFileSync(installMarkerPath(projectDirectory, plan), 'utf8').trim();\n } catch {\n return undefined;\n }\n}\n\nfunction installMarkerPath(projectDirectory: string, plan: DependencyPlan): string {\n return nodePath.join(projectDirectory, plan.installArtifact, INSTALL_MARKER_FILENAME);\n}\n\nexport function toDependencyReadinessState(\n readiness: DependencyReadiness,\n): Omit<DependencyReadinessState, 'updatedAt'> {\n return {\n status: readiness.status,\n reason: readiness.reason,\n fingerprint: readiness.fingerprint,\n installCommand: readiness.installCommand,\n };\n}\n\nfunction dependencyRecoveryCommand(readiness: DependencyReadiness): string {\n const { installCommand, plan, status } = readiness;\n if (installCommand === undefined) return 'install dependencies';\n\n // A version-bump pull changes the input fingerprint without changing resolved\n // dependencies, so the install reports \"no changes\" and does not refresh the\n // marker — which would otherwise leave this stale check looping. No package\n // manager offers a cheap \"lockfile already satisfied\" probe (pnpm#4861), so\n // remove the stale marker and touch the artifact after the install succeeds.\n // This also works when the current app still has an older PostToolUse hook\n // loaded and therefore cannot stamp the new fingerprint itself.\n if (status !== 'stale' || plan === undefined) return installCommand;\n return `${installCommand} && rm -f ${plan.installArtifact}/${INSTALL_MARKER_FILENAME} && touch ${plan.installArtifact}`;\n}\n\nexport function formatDependencyRecovery(readiness: DependencyReadiness): string {\n const problem =\n readiness.status === 'stale'\n ? \"the project's tool list changed since it was last set up, so safeword's checks may be out of date\"\n : \"this project's tools aren't installed yet, so safeword's checks can't run\";\n\n const lines = [\n `${problem}.`,\n // The recovery may end in a relative `touch`, so the folder has to be the\n // project root — \"the project folder\" reads as \"wherever you are\" inside a\n // monorepo package and quietly touches the wrong artifact.\n `Install them with this command from the project root folder, then try again:`,\n ` ${dependencyRecoveryCommand(readiness)}`,\n ];\n\n return lines.join('\\n');\n}\n\nfunction collectWorkspacePackageJsonPaths(\n projectDirectory: string,\n rootPackageJson: Record<string, unknown>,\n): string[] {\n return expandWorkspacePatterns(projectDirectory, readWorkspacePatterns(rootPackageJson));\n}\n\nfunction collectPnpmWorkspacePackageJsonPaths(projectDirectory: string): string[] {\n return expandWorkspacePatterns(projectDirectory, readPnpmWorkspacePackages(projectDirectory));\n}\n\n/**\n * Extract the `packages:` block-list globs from pnpm-workspace.yaml without a\n * YAML dependency (hooks are zero-third-party). Handles the standard block\n * sequence, quotes, comments, and `!` negation (passed through to\n * expandWorkspacePatterns). An inline flow sequence (`packages: [...]`) yields\n * nothing — uncommon in pnpm-workspace.yaml.\n */\nfunction readPnpmWorkspacePackages(projectDirectory: string): string[] {\n let content: string;\n try {\n content = readFileSync(nodePath.join(projectDirectory, 'pnpm-workspace.yaml'), 'utf8');\n } catch {\n return [];\n }\n\n const patterns: string[] = [];\n let insidePackages = false;\n for (const rawLine of content.split('\\n')) {\n const line = stripYamlComment(rawLine);\n if (!insidePackages) {\n if (/^packages:\\s*$/.test(line)) insidePackages = true;\n continue;\n }\n const item = line.match(/^\\s*-\\s*(.+?)\\s*$/);\n if (item?.[1] !== undefined) {\n patterns.push(stripYamlQuotes(item[1]));\n continue;\n }\n // A new top-level key (non-indented, non-comment, non-item) ends the block.\n if (/^[^\\s#-]/.test(line)) insidePackages = false;\n }\n return patterns;\n}\n\nfunction stripYamlComment(line: string): string {\n let quote: \"'\" | '\"' | undefined;\n for (let index = 0; index < line.length; index += 1) {\n const character = line[index];\n if (quote !== undefined) {\n if (character === quote && line[index - 1] !== '\\\\') quote = undefined;\n continue;\n }\n if (character === \"'\" || character === '\"') {\n quote = character;\n continue;\n }\n if (character === '#' && (index === 0 || /\\s/u.test(line[index - 1] ?? ''))) {\n return line.slice(0, index);\n }\n }\n return line;\n}\n\nfunction stripYamlQuotes(value: string): string {\n const trimmed = value.trim();\n const quoted =\n (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\")) ||\n (trimmed.startsWith('\"') && trimmed.endsWith('\"'));\n return quoted ? trimmed.slice(1, -1) : trimmed;\n}\n\nfunction readWorkspacePatterns(rootPackageJson: Record<string, unknown>): string[] {\n const rawWorkspaces = rootPackageJson.workspaces;\n\n if (Array.isArray(rawWorkspaces)) {\n return rawWorkspaces.filter((value): value is string => typeof value === 'string');\n }\n\n if (\n rawWorkspaces !== null &&\n typeof rawWorkspaces === 'object' &&\n Array.isArray((rawWorkspaces as { packages?: unknown }).packages)\n ) {\n return (rawWorkspaces as { packages: unknown[] }).packages.filter(\n (value): value is string => typeof value === 'string',\n );\n }\n\n return [];\n}\n\ninterface WorkspacePattern {\n pattern: string;\n negated: boolean;\n}\n\nfunction expandWorkspacePatterns(projectDirectory: string, rawPatterns: string[]): string[] {\n const patterns = rawPatterns\n .map(normalizeWorkspacePattern)\n .filter((pattern): pattern is WorkspacePattern => pattern !== undefined);\n const positivePatterns = patterns.filter(pattern => !pattern.negated);\n const negativePatterns = patterns.filter(pattern => pattern.negated);\n const packageJsonPaths = new Set<string>();\n\n for (const { pattern } of positivePatterns) {\n for (const packageJsonPath of expandPositiveWorkspacePattern(projectDirectory, pattern)) {\n if (!isExcludedWorkspacePackage(packageJsonPath, negativePatterns)) {\n packageJsonPaths.add(packageJsonPath);\n }\n }\n }\n\n return [...packageJsonPaths];\n}\n\nfunction normalizeWorkspacePattern(rawPattern: string): WorkspacePattern | undefined {\n let pattern = rawPattern.trim().replaceAll('\\\\', '/');\n const negated = pattern.startsWith('!');\n if (negated) pattern = pattern.slice(1);\n\n if (\n nodePath.posix.isAbsolute(pattern) ||\n /^[A-Za-z]:\\//.test(pattern) ||\n pattern.split('/').includes('..')\n ) {\n return undefined;\n }\n\n pattern = pattern.replace(/^\\.?\\//, '').replace(/\\/+$/, '');\n if (pattern.length === 0) return undefined;\n\n return { pattern, negated };\n}\n\nfunction expandPositiveWorkspacePattern(projectDirectory: string, pattern: string): string[] {\n if (!hasGlobSyntax(pattern)) {\n const packageJsonPath = pattern.endsWith('/package.json') ? pattern : `${pattern}/package.json`;\n const packageJsonFilePath = nodePath.resolve(projectDirectory, packageJsonPath);\n return existsSync(packageJsonFilePath) &&\n isProjectPathContained(projectDirectory, packageJsonFilePath)\n ? [packageJsonPath]\n : [];\n }\n\n return collectPackageJsonPathsUnder(\n projectDirectory,\n workspacePatternBaseDirectory(pattern),\n ).filter(packageJsonPath => matchesWorkspacePattern(pattern, packageJsonPath, true));\n}\n\nfunction collectPackageJsonPathsUnder(\n projectDirectory: string,\n relativeBaseDirectory: string,\n): string[] {\n const baseDirectory = nodePath.join(projectDirectory, relativeBaseDirectory);\n if (!isDirectory(baseDirectory) || !isProjectPathContained(projectDirectory, baseDirectory)) {\n return [];\n }\n\n const packageJsonPaths: string[] = [];\n const pendingDirectories = [baseDirectory];\n\n while (pendingDirectories.length > 0) {\n const directory = pendingDirectories.pop();\n if (directory === undefined) continue;\n\n const relativeDirectory = normalizeRelativePath(nodePath.relative(projectDirectory, directory));\n const packageJsonPath =\n relativeDirectory.length > 0 ? `${relativeDirectory}/package.json` : 'package.json';\n if (existsSync(nodePath.join(directory, 'package.json'))) {\n packageJsonPaths.push(packageJsonPath);\n }\n\n let entries: Dirent[];\n try {\n entries = readdirSync(directory, { withFileTypes: true });\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n if (!entry.isDirectory() || WORKSPACE_SCAN_EXCLUDED_DIRECTORIES.has(entry.name)) {\n continue;\n }\n pendingDirectories.push(nodePath.join(directory, entry.name));\n }\n }\n\n return packageJsonPaths;\n}\n\nfunction isProjectPathContained(projectDirectory: string, candidatePath: string): boolean {\n const resolvedProjectDirectory = nodePath.resolve(projectDirectory);\n const resolvedCandidatePath = nodePath.resolve(candidatePath);\n if (!isPathWithin(resolvedProjectDirectory, resolvedCandidatePath)) return false;\n if (!existsSync(resolvedCandidatePath)) return true;\n\n try {\n return isPathWithin(\n realpathSync(resolvedProjectDirectory),\n realpathSync(resolvedCandidatePath),\n );\n } catch {\n return false;\n }\n}\n\nfunction isPathWithin(parentPath: string, candidatePath: string): boolean {\n const relativePath = nodePath.relative(parentPath, candidatePath);\n return (\n relativePath === '' ||\n (!nodePath.isAbsolute(relativePath) &&\n relativePath !== '..' &&\n !relativePath.startsWith(`..${nodePath.sep}`))\n );\n}\n\nfunction isExcludedWorkspacePackage(\n packageJsonPath: string,\n negativePatterns: WorkspacePattern[],\n): boolean {\n return negativePatterns.some(({ pattern }) =>\n matchesWorkspacePattern(pattern, packageJsonPath, false),\n );\n}\n\nfunction matchesWorkspacePattern(\n pattern: string,\n packageJsonPath: string,\n unsupportedGlobDefault: boolean,\n): boolean {\n const target = pattern.endsWith('/package.json')\n ? packageJsonPath\n : packageJsonPath.replace(/\\/package\\.json$/, '');\n const matcher = workspacePatternMatcher(pattern);\n // Unsupported positive syntax errs toward fingerprinting too much, while\n // unsupported exclusions never hide a package from readiness tracking.\n if (matcher === undefined) return unsupportedGlobDefault;\n return matcher.test(target);\n}\n\nfunction workspacePatternMatcher(pattern: string): RegExp | undefined {\n if (/[?[\\]{}]/.test(pattern)) return undefined;\n\n let source = '^';\n for (let index = 0; index < pattern.length; index += 1) {\n const char = pattern[index];\n const next = pattern[index + 1];\n const afterNext = pattern[index + 2];\n if (char === undefined) continue;\n\n if (char === '*' && next === '*' && afterNext === '/') {\n source += '(?:.*/)?';\n index += 2;\n continue;\n }\n\n if (char === '*' && next === '*') {\n source += '.*';\n index += 1;\n continue;\n }\n\n if (char === '*') {\n source += '[^/]*';\n continue;\n }\n\n source += escapeRegExp(char);\n }\n\n return new RegExp(`${source}$`);\n}\n\nfunction workspacePatternBaseDirectory(pattern: string): string {\n const globIndex = firstGlobSyntaxIndex(pattern);\n if (globIndex === -1) {\n return pattern.endsWith('/package.json')\n ? normalizeRelativePath(nodePath.dirname(pattern))\n : pattern;\n }\n\n const staticPrefix = pattern.slice(0, globIndex);\n const slashIndex = staticPrefix.lastIndexOf('/');\n return slashIndex === -1 ? '' : staticPrefix.slice(0, slashIndex);\n}\n\nfunction firstGlobSyntaxIndex(pattern: string): number {\n const indexes = ['*', '?', '[', '{']\n .map(char => pattern.indexOf(char))\n .filter(index => index !== -1);\n return indexes.length === 0 ? -1 : Math.min(...indexes);\n}\n\nfunction hasGlobSyntax(pattern: string): boolean {\n return firstGlobSyntaxIndex(pattern) !== -1;\n}\n\nfunction normalizeRelativePath(path: string): string {\n return path.replaceAll('\\\\', '/');\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction isDependencyBackedSegment(segment: string): boolean {\n const [binary, ...args] = commandWords(segment);\n if (binary === undefined) return false;\n\n const basename = nodePath.basename(binary);\n\n if (binary.includes('node_modules/.bin/')) return true;\n\n if (basename === 'bun') {\n return isBunDependencyBackedCommand(args);\n }\n\n if (basename === 'bunx') return !isSafewordRecoverySegment(segment, args);\n\n if (basename === 'npx' || basename === 'pnpx' || basename === 'pnx') {\n return isKnownBinaryPackageExecutor(args);\n }\n\n if (basename === 'npm') {\n return isNpmDependencyBackedCommand(args);\n }\n\n if (basename === 'pnpm' || basename === 'yarn') {\n return isPackageManagerDependencyBackedCommand(args);\n }\n\n return DEPENDENCY_BINARIES.has(basename);\n}\n\nfunction isSafewordRecoverySegment(segment: string, args: string[]): boolean {\n if (containsShellEvaluationSyntax(segment)) return false;\n\n let packageIndex = 0;\n while (BUNX_BOOLEAN_OPTIONS.has(args[packageIndex] ?? '')) packageIndex += 1;\n if (args[packageIndex] === '--') packageIndex += 1;\n\n const packageSpecifier = args[packageIndex];\n if (packageSpecifier === undefined || !/^safeword(?:@[^/@\\s]+)?$/.test(packageSpecifier)) {\n return false;\n }\n\n const command = firstSafewordCommandArgument(args.slice(packageIndex + 1));\n return command !== undefined && SAFEWORD_RECOVERY_COMMANDS.has(command);\n}\n\nfunction containsShellEvaluationSyntax(segment: string): boolean {\n let quote: '\"' | \"'\" | undefined;\n let escaped = false;\n\n for (const char of segment) {\n if (escaped) {\n escaped = false;\n continue;\n }\n if (char === '\\\\' && quote !== \"'\") {\n escaped = true;\n continue;\n }\n if (char === \"'\") {\n if (quote === undefined) quote = char;\n else if (quote === char) quote = undefined;\n continue;\n }\n if (char === '\"') {\n if (quote === undefined) quote = char;\n else if (quote === char) quote = undefined;\n continue;\n }\n if (quote !== \"'\" && (char === '$' || char === '`')) return true;\n if (quote === undefined && (char === '<' || char === '>' || char === '&')) return true;\n }\n\n return false;\n}\n\n// Deliberately not `firstCommandArgument`: that helper skips over options it\n// does not recognize, which would let an unknown flag carry an argument the\n// classifier then mistakes for a recovery verb. Here an unrecognized option\n// means \"not a recovery shape\", so the segment falls through to the guard.\nfunction firstSafewordCommandArgument(args: string[]): string | undefined {\n for (let index = 0; index < args.length; index += 1) {\n const arg = args[index];\n if (arg === undefined) continue;\n\n if (arg === '--') return args[index + 1];\n if (!arg.startsWith('-') || arg === '-') return arg;\n\n const option = arg.split('=')[0] ?? arg;\n if (SAFEWORD_GLOBAL_OPTIONS_WITH_VALUES.has(option)) {\n if (!arg.includes('=')) index += 1;\n continue;\n }\n if (!SAFEWORD_GLOBAL_BOOLEAN_OPTIONS.has(arg)) return undefined;\n }\n\n return undefined;\n}\n\nfunction isBunDependencyBackedCommand(args: string[]): boolean {\n const subcommand = firstCommandArgument(args, BUN_OPTIONS_WITH_VALUES);\n return isPackageScriptCommand(subcommand);\n}\n\nfunction isNpmDependencyBackedCommand(args: string[]): boolean {\n const subcommand = firstCommandArgument(args, PACKAGE_MANAGER_OPTIONS_WITH_VALUES);\n return isPackageScriptCommand(subcommand) || subcommand === 'exec';\n}\n\nfunction isPackageManagerDependencyBackedCommand(args: string[]): boolean {\n const subcommand = firstCommandArgument(args, PACKAGE_MANAGER_OPTIONS_WITH_VALUES);\n return (\n isPackageScriptCommand(subcommand) ||\n subcommand === 'exec' ||\n (subcommand !== undefined && DEPENDENCY_BINARIES.has(subcommand))\n );\n}\n\nfunction isPackageScriptCommand(command: string | undefined): boolean {\n return command !== undefined && PACKAGE_SCRIPT_COMMANDS.has(command);\n}\n\nfunction isKnownBinaryPackageExecutor(args: string[]): boolean {\n const target = firstCommandArgument(args, PACKAGE_MANAGER_OPTIONS_WITH_VALUES);\n return target !== undefined && DEPENDENCY_BINARIES.has(target);\n}\n\nfunction firstCommandArgument(\n args: string[],\n optionsWithValues: ReadonlySet<string>,\n): string | undefined {\n for (let index = 0; index < args.length; index += 1) {\n const arg = args[index];\n if (arg === undefined) continue;\n\n if (arg === '--') {\n return args[index + 1];\n }\n\n if (!arg.startsWith('-') || arg === '-') {\n return arg;\n }\n\n if (optionsWithValues.has(arg) && !arg.includes('=')) {\n index += 1;\n }\n }\n\n return undefined;\n}\n\nfunction isInstallArtifactStale(\n projectDirectory: string,\n plan: DependencyPlan,\n artifactPath: string,\n): boolean {\n const artifactMtime = getMtimeMs(artifactPath);\n if (artifactMtime === undefined) return true;\n\n const latestInputMtime = Math.max(\n ...plan.inputPaths.map(\n inputPath => getMtimeMs(nodePath.join(projectDirectory, inputPath)) ?? 0,\n ),\n );\n\n return artifactMtime + 1000 < latestInputMtime;\n}\n\nfunction readJsonFile<T>(filePath: string): T | undefined {\n try {\n return JSON.parse(readFileSync(filePath, 'utf8')) as T;\n } catch {\n return undefined;\n }\n}\n\nfunction isDirectory(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\nfunction getMtimeMs(path: string): number | undefined {\n try {\n return statSync(path).mtimeMs;\n } catch {\n return undefined;\n }\n}\n\nfunction uniqueSorted(values: string[]): string[] {\n return [...new Set(values)].toSorted();\n}\n\n/**\n * Directory holding committed git hooks. Husky wires git's `core.hooksPath` to\n * `.husky/_` during `prepare`, which only runs on `npm install` — so a fresh\n * clone/worktree has no hooks wired until deps are installed, and every committed\n * pre-commit guard silently does not run (#364).\n */\nexport const COMMITTED_HOOKS_DIR = '.husky';\n\nexport interface GitHooksWiringInput {\n /** A committed hook (`.husky/pre-commit`) exists in the repo. */\n committedHookExists: boolean;\n /** Current value of git `core.hooksPath` (`''` when unset). */\n currentHooksPath: string;\n /** Whether the directory git's `core.hooksPath` points at holds a usable hook. */\n currentHooksPathActive: boolean;\n}\n\nexport interface GitHooksWiringDecision {\n action: 'none' | 'wire';\n hooksPath?: string;\n}\n\n/**\n * Whether `core.hooksPath` is unset or husky-managed (so safeword may wire it).\n * A non-empty, non-husky value is a deliberate custom hooks path we must not\n * clobber, even when it has no `pre-commit` — the user owns it.\n */\nfunction isHuskyManagedHooksPath(hooksPath: string): boolean {\n const normalized = hooksPath.replace(/\\/+$/, '');\n return normalized === '' || normalized === COMMITTED_HOOKS_DIR || normalized === '.husky/_';\n}\n\n/**\n * Decide whether to wire git hooks. When a committed `.husky/pre-commit` exists but\n * `core.hooksPath` is unset (or already husky-managed) and has no usable hook, wire\n * it to `.husky` so the committed guard fires — the absence of enforcement becomes\n * self-enforcing. Husky resets `core.hooksPath` to `.husky/_` on its next install,\n * so this is a safe bridge for the fresh-clone window. A deliberate custom\n * `core.hooksPath` is left untouched.\n */\nexport function decideGitHooksWiring(input: GitHooksWiringInput): GitHooksWiringDecision {\n if (!input.committedHookExists) return { action: 'none' };\n if (input.currentHooksPathActive) return { action: 'none' };\n if (!isHuskyManagedHooksPath(input.currentHooksPath)) return { action: 'none' };\n return { action: 'wire', hooksPath: COMMITTED_HOOKS_DIR };\n}\n\nexport function readGitHooksPath(cwd: string): string {\n const result = spawnSync('git', ['config', '--get', 'core.hooksPath'], {\n cwd,\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n return result.status === 0 ? result.stdout.trim() : '';\n}\n\nexport function wireGitHooksIfNeeded(cwd: string): void {\n const committedHookExists = existsSync(nodePath.join(cwd, COMMITTED_HOOKS_DIR, 'pre-commit'));\n const currentHooksPath = readGitHooksPath(cwd);\n const currentHooksPathActive =\n currentHooksPath !== '' && existsSync(nodePath.resolve(cwd, currentHooksPath, 'pre-commit'));\n const decision = decideGitHooksWiring({\n committedHookExists,\n currentHooksPath,\n currentHooksPathActive,\n });\n if (decision.action !== 'wire' || decision.hooksPath === undefined) return;\n spawnSync('git', ['config', 'core.hooksPath', decision.hooksPath], { cwd, stdio: 'ignore' });\n}\n","// Safeword: namespace-root resolver (hook-side copy, ticket TAGWZ8).\n//\n// Resolves the directory holding safeword's project knowledge (tickets,\n// learnings, personas, glossary, surfaces, architecture). Precedence (epic AQJ95G):\n// explicit config `paths.projectRoot` in `.safeword/config.json` →\n// `.project/` → legacy `.safeword-project/`; neither present → `.project/`.\n//\n// Deliberate duplicate of `resolveNamespaceRoot` in the CLI's\n// `src/utils/configured-paths.ts` — hooks run standalone under bun in\n// customer repos with no import path to the CLI. A differential test pins\n// the two copies against shared fixtures (P58R22 pattern).\n\nimport { existsSync, readFileSync, statSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nexport const NAMESPACE_ROOT_DEFAULT = '.project';\nexport const NAMESPACE_ROOT_LEGACY = '.safeword-project';\n\n/** True after explicit Safeword setup has enrolled this repository. */\nexport function hasSafewordProjectMarker(projectDirectory: string): boolean {\n return existsSync(nodePath.join(projectDirectory, '.safeword', 'SAFEWORD.md'));\n}\n\n/**\n * The raw non-empty `paths.<key>` string from `.safeword/config.json`, or\n * `undefined` (unset, empty, non-string, or missing/unparseable config).\n * Shared by the hook-side path resolvers (projectRoot here, architecture in\n * architecture-document-nudge.ts) — hooks run standalone, so this is their\n * one config-reading seam.\n */\nexport function readConfiguredPathValue(projectDirectory: string, key: string): string | undefined {\n const configPath = nodePath.join(projectDirectory, '.safeword', 'config.json');\n if (!existsSync(configPath)) return undefined;\n\n let parsed: { paths?: Record<string, unknown> };\n try {\n parsed = JSON.parse(readFileSync(configPath, 'utf8')) as { paths?: Record<string, unknown> };\n } catch {\n return undefined;\n }\n\n const raw = parsed.paths?.[key];\n if (typeof raw !== 'string' || raw.length === 0) return undefined;\n return raw;\n}\n\n/** Resolve a configured project path or derive its default from the namespace root. */\nexport function resolveConfiguredPath(\n projectDirectory: string,\n key: string,\n defaultBasename = `${key}.md`,\n): string {\n const configured = readConfiguredPathValue(projectDirectory, key);\n if (configured === undefined) {\n return nodePath.join(resolveNamespaceRoot(projectDirectory), defaultBasename);\n }\n return nodePath.isAbsolute(configured) ? configured : nodePath.join(projectDirectory, configured);\n}\n\nfunction readConfiguredProjectRoot(projectDirectory: string): string | undefined {\n return readConfiguredPathValue(projectDirectory, 'projectRoot');\n}\n\n/**\n * True when `filePath` lies under `<namespace root>/<subpath>` for either\n * the default or the legacy root. String-level check for hook filters that\n * receive edited-file paths in unknown (absolute or relative) form.\n */\nexport function isNamespacePath(filePath: string, subpath: string): boolean {\n return [NAMESPACE_ROOT_DEFAULT, NAMESPACE_ROOT_LEGACY].some(root => {\n const needle = `${root}/${subpath}`;\n // Boundary-anchored: the root must start the path or follow a separator,\n // so `foo.project/…` never matches the `.project/` root.\n return filePath.startsWith(needle) || filePath.includes(`/${needle}`);\n });\n}\n\n/** True when `path` exists and is a directory (a stray file is not a root). */\nfunction isDirectory(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\n/** Absolute path of the resolved namespace root for `projectDirectory`. */\nexport function resolveNamespaceRoot(projectDirectory: string): string {\n const configured = readConfiguredProjectRoot(projectDirectory);\n if (configured !== undefined) {\n return nodePath.isAbsolute(configured)\n ? configured\n : nodePath.join(projectDirectory, configured);\n }\n\n const defaultRoot = nodePath.join(projectDirectory, NAMESPACE_ROOT_DEFAULT);\n if (isDirectory(defaultRoot)) return defaultRoot;\n\n const legacyRoot = nodePath.join(projectDirectory, NAMESPACE_ROOT_LEGACY);\n if (isDirectory(legacyRoot)) return legacyRoot;\n\n return defaultRoot;\n}\n","// The single shell-command tokenizer shared by every Bash gate predicate\n// (process-kill-guard, bash-ledger-writes, dependency-readiness, and the\n// cursor gate-adapter — cursor/ imports from lib/, never the reverse).\n// Unified from two divergent copies in ticket EDDABK: a change here shifts\n// what EVERY gate catches, so behavior is pinned directly in\n// tests/hooks/shell-segments.test.ts as well as by each gate's own tests.\n//\n// This is NOT a shell parser. It splits a command string on unquoted segment\n// boundaries (`;`, newline, `&`, `&&`, `||`, single `|`) and whitespace-splits\n// each segment honoring quotes and backslash escapes. Backslash is literal\n// inside single quotes (POSIX); `>|` is a clobbering redirection operator,\n// not a pipe boundary. Expansions, substitutions, and redirections are left\n// as literal tokens for the caller to classify.\n\nimport nodePath from 'node:path';\n\nexport type ShellControlOperator = '&' | '&&' | '||' | ';' | '|' | '|&';\n\nexport interface ShellCommandSegment {\n command: string;\n operatorAfter?: ShellControlOperator;\n}\n\n/** Split a command list without discarding the control operator after each segment. */\nexport function parseShellCommandList(command: string): ShellCommandSegment[] {\n const segments: ShellCommandSegment[] = [];\n let current = '';\n let quote: '\"' | \"'\" | undefined;\n let escaped = false;\n\n for (let index = 0; index < command.length; index += 1) {\n const char = command[index];\n if (char === undefined) continue;\n\n if (escaped) {\n current += char;\n escaped = false;\n continue;\n }\n if (char === '\\\\' && quote !== \"'\") {\n // A backslash-newline is a bash line continuation: both characters are\n // removed and the surrounding text joins, so `pkill \\<newline>node`\n // runs `pkill node` — drop both rather than letting the literal newline\n // fuse onto the next word and hide it from the gates.\n if (command[index + 1] === '\\n') {\n index += 1;\n continue;\n }\n current += char;\n escaped = true;\n continue;\n }\n if (quote !== undefined) {\n current += char;\n if (char === quote) quote = undefined;\n continue;\n }\n if (char === '\"' || char === \"'\") {\n quote = char;\n current += char;\n continue;\n }\n\n const next = command[index + 1];\n if (char === ';' || char === '\\n') {\n pushCommandSegment(segments, current, ';');\n current = '';\n continue;\n }\n if ((char === '&' && next === '&') || (char === '|' && next === '|')) {\n pushCommandSegment(segments, current, char === '&' ? '&&' : '||');\n current = '';\n index += 1;\n continue;\n }\n // A single `&` backgrounds the preceding pipeline. Keep file-descriptor\n // redirections (`2>&1`, `<&3`, `&>log`) intact: their ampersand touches\n // `<`/`>` or introduces `>`.\n if (char === '&' && command[index - 1] !== '>' && command[index - 1] !== '<' && next !== '>') {\n pushCommandSegment(segments, current, '&');\n current = '';\n continue;\n }\n // A single `|` is a pipe boundary, and so is `|&` (bash's stdout+stderr\n // pipe) — consume its trailing `&` so it doesn't become the next segment's\n // phantom command word. `>|` / `>|&` are clobber redirections, not pipes.\n if (char === '|' && command[index - 1] !== '>') {\n pushCommandSegment(segments, current, next === '&' ? '|&' : '|');\n current = '';\n if (next === '&') index += 1;\n continue;\n }\n\n current += char;\n }\n\n pushCommandSegment(segments, current);\n return segments;\n}\n\nexport function splitShellSegments(command: string): string[] {\n return parseShellCommandList(command).map(segment => segment.command);\n}\n\nfunction pushCommandSegment(\n segments: ShellCommandSegment[],\n segment: string,\n operatorAfter?: ShellControlOperator,\n): void {\n const trimmed = segment.trim();\n if (trimmed.length > 0) {\n segments.push(\n operatorAfter === undefined ? { command: trimmed } : { command: trimmed, operatorAfter },\n );\n }\n}\n\nexport function parseShellWords(segment: string): string[] {\n const words: string[] = [];\n let current = '';\n let quote: '\"' | \"'\" | undefined;\n let escaped = false;\n\n for (const char of segment) {\n if (escaped) {\n current += char;\n escaped = false;\n continue;\n }\n // Backslash is literal inside single quotes (POSIX), an escape elsewhere.\n if (char === '\\\\' && quote !== \"'\") {\n escaped = true;\n continue;\n }\n if ((char === '\"' || char === \"'\") && quote === undefined) {\n quote = char;\n continue;\n }\n if (char === quote) {\n quote = undefined;\n continue;\n }\n if (quote === undefined && /\\s/.test(char)) {\n if (current !== '') {\n words.push(current);\n current = '';\n }\n continue;\n }\n current += char;\n }\n\n if (current !== '') words.push(current);\n return words;\n}\n\n/** `env` options whose value is a separate following word. */\nconst ENV_OPTIONS_WITH_VALUES = new Set(['--argv0', '--chdir', '--unset', '-a', '-C', '-u']);\n\n/**\n * Index of the first word that is the actual command name. Skips a leading\n * run of subshell/group openers (`(` / `{`), then loops over execution\n * prefixes until a real word remains: `VAR=val` environment assignments, the\n * `command` builtin, `env` (matched by basename, with its options —\n * value-taking ones via ENV_OPTIONS_WITH_VALUES — interleaved assignments,\n * and a `--` terminator), and the `corepack` launcher (matched by basename).\n * Skipping the opener means `( git commit )` resolves to the real command\n * word, not `(`; looping means `FOO=1 env BAR=2 corepack pnpm install`\n * resolves to `pnpm`.\n *\n * Deliberately NOT skipped (pre-existing behavior): `sudo` (the kill-guard\n * skips it itself), `nohup`/`nice`/`time`/`xargs`.\n */\nexport function commandWordIndex(words: string[]): number {\n let index = 0;\n while (words[index] === '(' || words[index] === '{') index += 1;\n\n while (index < words.length) {\n index = skipEnvironmentAssignments(words, index);\n const word = words[index];\n if (word === undefined) return index;\n if (word === 'command') {\n index += 1;\n // `command -p CMD` runs CMD with the default PATH, so CMD is the real\n // command word — skip the `-p`. `command -v`/`-V` DESCRIBE without\n // running, so they are left in place and never mistaken for a run of\n // the command they name.\n while (words[index] === '-p') index += 1;\n continue;\n }\n const binary = nodePath.basename(word);\n if (binary === 'env') {\n index = skipEnvOptions(words, index + 1);\n continue;\n }\n if (binary === 'corepack') {\n index += 1;\n continue;\n }\n return index;\n }\n\n return index;\n}\n\n/**\n * A segment's words with leading execution prefixes stripped — the command\n * word first, then its arguments. Composes parseShellWords with\n * commandWordIndex; use it when a caller only needs the resolved argv and not\n * the index (the kill/ledger/cursor gates keep the index for in-place\n * iteration).\n */\nexport function commandWords(segment: string): string[] {\n const words = parseShellWords(segment);\n return words.slice(commandWordIndex(words));\n}\n\nfunction skipEnvOptions(words: string[], start: number): number {\n let index = start;\n while (index < words.length) {\n const word = words[index] ?? '';\n if (isEnvironmentAssignment(word)) {\n index += 1;\n continue;\n }\n if (word === '--') {\n index += 1;\n break;\n }\n if (ENV_OPTIONS_WITH_VALUES.has(word) && !word.includes('=')) {\n index += 2;\n continue;\n }\n if (word.startsWith('-')) {\n index += 1;\n continue;\n }\n break;\n }\n return index;\n}\n\nfunction skipEnvironmentAssignments(words: string[], start: number): number {\n let index = start;\n while (index < words.length && isEnvironmentAssignment(words[index] ?? '')) {\n index += 1;\n }\n return index;\n}\n\nfunction isEnvironmentAssignment(token: string): boolean {\n return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);\n}\n","/**\n * Test runner utilities for the stop hook.\n *\n * The per-language test command is resolved by the single source of truth —\n * `safeword project test-plan --kind test --json` — not duplicated here. This hook only\n * EXECUTES the resolved commands (timeout-safe, no zombies) and appends the JS\n * acceptance lane (`test:bdd`), which the resolver does not emit.\n *\n * Shipped hooks cannot import safeword code, so we reach the resolver via the\n * CLI (the `safewordCliCommand()` installed→source→bunx pattern, mirroring lint.ts).\n */\n\nimport { execSync, spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\ntype PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun';\n\ntype TestCommand = {\n /** Label for output/diagnostics (the runner or script name). */\n script: string;\n /** Concrete shell command to execute. */\n command: string;\n /** Directory to run the command in (the resolved entry's cwd). */\n cwd: string;\n};\n\n/** One entry of the schema-1 `safeword project test-plan --json` result envelope. */\ninterface PlanEntry {\n language: string;\n cwd: string;\n command: string;\n runner: string;\n available: boolean;\n}\n\ninterface TestPlanEnvelope {\n schema_version: 1;\n data?: {\n plan?: PlanEntry[];\n };\n}\n\nexport interface TestResult {\n /** Whether tests passed (exit code 0). */\n passed: boolean;\n /** Truncated output from the test run. */\n output: string;\n /** true if no test command was found — caller should skip, not block. */\n skipped: boolean;\n /**\n * true when a command failed because its binary was not found (exit 127 /\n * \"command not found\") — an environment problem (uninstalled toolchain), not a\n * red test. Lets the caller surface an install recovery instead of \"tests\n * failed\". (Issue #325.)\n */\n toolchainMissing?: boolean;\n}\n\n/** Fast feedback cap for test-plan commands (60 seconds). */\nconst TEST_TIMEOUT_MS = 60_000;\n\n/**\n * The explicit acceptance lane can be materially slower than its unit-test\n * counterpart. Keep it bounded below Codex Stop's 600-second timeout without\n * rejecting a passing BDD suite solely for taking longer than a minute.\n */\nconst BDD_TEST_TIMEOUT_MS = 5 * 60_000;\n\n/** Resolve the bounded execution budget for a planned test command. */\nexport function timeoutMsForTestCommand(script: string): number {\n return script === 'test:bdd' ? BDD_TEST_TIMEOUT_MS : TEST_TIMEOUT_MS;\n}\n\n/** Maximum lines of test output to inject into the block reason. */\nconst MAX_OUTPUT_LINES = 30;\n\n/** Maximum characters of test output to inject into the block reason. */\nconst MAX_OUTPUT_CHARS = 3000;\n\nconst projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();\n\n/**\n * Detect package manager by lockfile presence (bun > pnpm > yarn > npm).\n * Used only for the `test:bdd` acceptance lane (the resolver owns the rest).\n */\nfunction detectPackageManager(cwd: string): PackageManager {\n if (existsSync(nodePath.join(cwd, 'bun.lockb')) || existsSync(nodePath.join(cwd, 'bun.lock')))\n return 'bun';\n if (existsSync(nodePath.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';\n if (existsSync(nodePath.join(cwd, 'yarn.lock'))) return 'yarn';\n if (existsSync(nodePath.join(cwd, 'package-lock.json'))) return 'npm';\n if (process.versions.bun) return 'bun';\n return 'npm';\n}\n\n/** Convert a package.json script name into the detected package manager's run command. */\nfunction formatRunCommand(script: string, packageManager: PackageManager): string {\n if (packageManager === 'npm') return script === 'test' ? 'npm test' : `npm run ${script}`;\n return `${packageManager} run ${script}`;\n}\n\n/**\n * Resolve the safeword CLI invocation. `SAFEWORD_CLI` (a path to cli.js/cli.ts run\n * via bun) overrides for tests/dev; otherwise the installed package, then the\n * dogfood source, then `bunx`.\n */\nfunction safewordCliCommand(cwd: string): [string, ...string[]] {\n const override = process.env.SAFEWORD_CLI;\n if (override) return ['bun', override];\n const installed = nodePath.join(cwd, 'node_modules', 'safeword', 'dist', 'cli.js');\n if (existsSync(installed)) return ['bun', installed];\n const source = nodePath.join(cwd, 'packages', 'cli', 'src', 'cli.ts');\n if (existsSync(source)) return ['bun', source];\n return ['bunx', 'safeword'];\n}\n\n/**\n * Ask `safeword project test-plan` for the project's test commands and keep the runnable\n * (available) ones. Returns [] on any failure so the caller skips, never blocks.\n */\nfunction resolvePlanCommands(cwd: string): TestCommand[] {\n const cli = safewordCliCommand(cwd);\n const result = spawnSync(\n cli[0],\n [...cli.slice(1), 'project', 'test-plan', '--kind', 'test', '--json', cwd],\n { encoding: 'utf8', timeout: TEST_TIMEOUT_MS },\n );\n if (result.status !== 0 || !result.stdout) return [];\n try {\n const envelope = JSON.parse(result.stdout) as TestPlanEnvelope;\n const entries = envelope.data?.plan ?? [];\n return entries\n .filter(entry => entry.available)\n .map(entry => ({ script: entry.runner, command: entry.command, cwd: entry.cwd }));\n } catch {\n return [];\n }\n}\n\n/** The JS acceptance lane (`test:bdd`) — consumer-side, not emitted by the resolver. */\nfunction bddCommand(cwd: string): TestCommand | undefined {\n try {\n const pkg = JSON.parse(readFileSync(nodePath.join(cwd, 'package.json'), 'utf8')) as {\n scripts?: Record<string, string>;\n };\n if (pkg.scripts?.['test:bdd']) {\n return {\n script: 'test:bdd',\n command: formatRunCommand('test:bdd', detectPackageManager(cwd)),\n cwd,\n };\n }\n } catch {\n // No package.json — no acceptance lane.\n }\n return undefined;\n}\n\n/** Resolved suite (from test-plan) plus the acceptance lane; [] means skip. */\nfunction getTestCommands(cwd: string): TestCommand[] {\n const commands = resolvePlanCommands(cwd);\n const bdd = bddCommand(cwd);\n if (bdd) commands.push(bdd);\n return commands;\n}\n\nfunction formatCommandOutput(testCommand: TestCommand, output: string): string {\n const trimmed = output.trimEnd();\n return [`$ ${testCommand.command}`, trimmed].filter(Boolean).join('\\n');\n}\n\n/**\n * Truncate output to the last N lines and at most M characters.\n * Test failures print the summary at the end — we want the tail.\n */\nfunction truncateOutput(output: string): string {\n const lines = output.trimEnd().split('\\n');\n const tail = lines.slice(-MAX_OUTPUT_LINES).join('\\n');\n if (tail.length <= MAX_OUTPUT_CHARS) return tail;\n // Character cap: take the last MAX_OUTPUT_CHARS characters\n return '...(truncated)\\n' + tail.slice(-MAX_OUTPUT_CHARS);\n}\n\n/**\n * Test commands are application processes, not hook children. A Codex Stop\n * handler carries its own runtime identity in these variables; forwarding them\n * makes tests that intentionally exercise another runtime misidentify itself.\n */\nfunction testSubprocessEnvironment(): NodeJS.ProcessEnv {\n const environment = { ...process.env };\n delete environment.SAFEWORD_AGENT_RUNTIME;\n delete environment.CODEX_THREAD_ID;\n return environment;\n}\n\nfunction runSingleTestCommand(testCommand: TestCommand): {\n passed: boolean;\n output: string;\n toolchainMissing?: boolean;\n} {\n const timeoutMs = timeoutMsForTestCommand(testCommand.script);\n try {\n const output = execSync(testCommand.command, {\n cwd: testCommand.cwd,\n timeout: timeoutMs,\n stdio: 'pipe',\n encoding: 'utf8',\n env: testSubprocessEnvironment(),\n });\n return { passed: true, output: formatCommandOutput(testCommand, output) };\n } catch (error) {\n const err = error as NodeJS.ErrnoException & {\n status?: number;\n stdout?: string;\n stderr?: string;\n killed?: boolean;\n };\n\n if (err.killed) {\n return {\n passed: false,\n output: `$ ${testCommand.command}\\n${testCommand.script} timed out after ${\n timeoutMs / 1000\n }s — tests may be too slow or the runner hung.`,\n };\n }\n\n const combined = (err.stdout ?? '') + (err.stderr ?? '');\n // Exit 127 (or a shell \"command not found\") means the runner binary is\n // absent — an uninstalled toolchain, not a failing test.\n const toolchainMissing = err.status === 127 || /command not found|: not found/i.test(combined);\n return {\n passed: false,\n output: formatCommandOutput(\n testCommand,\n combined || `${testCommand.script} exited with non-zero status`,\n ),\n toolchainMissing,\n };\n }\n}\n\n/**\n * Run the project's test suite and return the result.\n *\n * - Resolves commands from `safeword project test-plan` (single source of truth) + the\n * `test:bdd` acceptance lane.\n * - Uses execSync for synchronous, timeout-safe execution (no zombie processes).\n * - Returns skipped=true if no runnable command was found (caller should not block).\n */\nexport function runTests(cwd: string = projectDir): TestResult {\n const commands = getTestCommands(cwd);\n if (commands.length === 0) return { passed: true, output: '', skipped: true };\n\n const outputs: string[] = [];\n\n for (const testCommand of commands) {\n const result = runSingleTestCommand(testCommand);\n outputs.push(result.output);\n if (!result.passed)\n return {\n passed: false,\n output: truncateOutput(outputs.join('\\n\\n')),\n skipped: false,\n toolchainMissing: result.toolchainMissing,\n };\n }\n\n return { passed: true, output: truncateOutput(outputs.join('\\n\\n')), skipped: false };\n}\n","// Done-gate annotation ledger validator (ticket J7VBGJ, Rules 3 + 4).\n// Pure function over test-definitions.md content + an injected SHA resolver.\n// Returns { ok, errors[] }. The wiring layer in stop-quality.ts provides a\n// Git-backed resolver that can canonicalize rebased commits.\n\nimport {\n classifyAnnotation,\n isValidSha,\n isValidSkipReason,\n parseCheckboxAnnotation,\n type CheckboxAnnotation,\n} from './parse-annotation.js';\n\nexport interface LedgerValidationResult {\n ok: boolean;\n errors: string[];\n}\n\nexport type ShaResolution = boolean | string;\nexport type ShaResolver = (sha: string) => ShaResolution;\n\ninterface ScenarioLedger {\n name: string;\n red?: CheckboxAnnotation;\n green?: CheckboxAnnotation;\n refactor?: CheckboxAnnotation;\n}\n\nfunction parseLedger(content: string): {\n scenarios: ScenarioLedger[];\n crossScenario?: CheckboxAnnotation;\n} {\n const lines = content.split('\\n');\n const scenarios: ScenarioLedger[] = [];\n let current: ScenarioLedger | undefined;\n let crossScenario: CheckboxAnnotation | undefined;\n\n for (const line of lines) {\n const scenarioMatch = /^#{2,3}\\s+Scenario:\\s*(.+)$/.exec(line);\n if (scenarioMatch) {\n current = { name: (scenarioMatch[1] ?? '').trim() };\n scenarios.push(current);\n continue;\n }\n const parsed = parseCheckboxAnnotation(line);\n if (!parsed) continue;\n if (parsed.step === 'cross-scenario') {\n crossScenario = parsed;\n continue;\n }\n if (current) {\n if (parsed.step === 'RED') current.red = parsed;\n else if (parsed.step === 'GREEN') current.green = parsed;\n else if (parsed.step === 'REFACTOR') current.refactor = parsed;\n }\n }\n\n return { scenarios, crossScenario };\n}\n\n/**\n * Validate a SHA-position annotation: format first (7-40 hex), then reachability\n * via the injected oracle. Returns the error message plus whether it's a\n * *format* error (so a caller can skip downstream collision tracking on a\n * malformed value), or null if the SHA resolves. Centralizes the two wordings so\n * the per-scenario and cross-scenario rows can't drift.\n */\nfunction checkSha(\n value: string,\n resolveSha: ShaResolver,\n label: string,\n): { error?: string; format: boolean; canonicalSha?: string } {\n if (!isValidSha(value)) {\n return {\n error: `${label}: \"${value}\" is not a valid commit SHA (expected 7-40 hex chars).`,\n format: true,\n };\n }\n\n const resolution = resolveSha(value);\n if (resolution === false) {\n return { error: `${label}: SHA ${value} is not reachable from HEAD.`, format: false };\n }\n\n return {\n format: false,\n canonicalSha: resolution === true ? value : resolution,\n };\n}\n\nfunction validateScenario(\n scenario: ScenarioLedger,\n resolveSha: ShaResolver,\n errors: string[],\n): void {\n const steps: Array<{ name: string; box?: CheckboxAnnotation }> = [\n { name: 'RED', box: scenario.red },\n { name: 'GREEN', box: scenario.green },\n { name: 'REFACTOR', box: scenario.refactor },\n ];\n\n // Legacy detection: if every present checkbox has no annotation, this is a\n // pre-feature scenario — silently accepted (forward-looking rule).\n const annotatedSteps = steps.filter(s => s.box && s.box.annotation !== '');\n if (annotatedSteps.length === 0) return;\n\n const shaToStep = new Map<string, string>();\n let realShaCount = 0;\n\n for (const step of steps) {\n if (!step.box) continue;\n const kind = classifyAnnotation(step.box.annotation);\n\n if (kind.kind === 'none') continue; // legacy slot — silent\n\n if (kind.kind === 'skip') {\n if (!isValidSkipReason(kind.reason)) {\n errors.push(\n `Scenario \"${scenario.name}\" ${step.name}: skip reason is empty or whitespace-only. Provide a real reason.`,\n );\n }\n continue;\n }\n\n // SHA candidate\n realShaCount++;\n const checkedSha = checkSha(kind.value, resolveSha, `Scenario \"${scenario.name}\" ${step.name}`);\n if (checkedSha.error) {\n errors.push(checkedSha.error);\n if (checkedSha.format) continue; // malformed — don't track it for collisions\n }\n if (!checkedSha.canonicalSha) continue;\n const existingStep = shaToStep.get(checkedSha.canonicalSha);\n if (existingStep) {\n errors.push(\n `Scenario \"${scenario.name}\" SHA collision: ${existingStep} and ${step.name} share SHA ${checkedSha.canonicalSha}. Each TDD step must correspond to a distinct commit.`,\n );\n } else {\n shaToStep.set(checkedSha.canonicalSha, step.name);\n }\n }\n\n if (realShaCount === 0) {\n errors.push(\n `Scenario \"${scenario.name}\" represents work that produced no commits — all steps are skipped. At least one step must carry a real SHA.`,\n );\n }\n}\n\nfunction validateCrossScenario(\n cs: CheckboxAnnotation | undefined,\n resolveSha: ShaResolver,\n errors: string[],\n): void {\n if (!cs) {\n errors.push(\n 'Cross-scenario refactor row is missing. Add `- [ ] cross-scenario` to test-definitions.md and complete it with a SHA or `skip: <reason>` before done.',\n );\n return;\n }\n\n if (!cs.checked) {\n errors.push(\n 'Cross-scenario refactor row is unchecked. Mark it with a SHA or `skip: <reason>` before done.',\n );\n return;\n }\n\n if (cs.annotation === '') return; // legacy bare — silently OK\n\n const kind = classifyAnnotation(cs.annotation);\n if (kind.kind === 'skip') {\n if (!isValidSkipReason(kind.reason)) {\n errors.push(\n 'Cross-scenario refactor row: skip reason is empty or whitespace-only. Provide a real reason.',\n );\n }\n return;\n }\n\n if (kind.kind === 'sha') {\n const checkedSha = checkSha(kind.value, resolveSha, 'Cross-scenario refactor row');\n if (checkedSha.error) errors.push(checkedSha.error);\n }\n}\n\n/**\n * Whether the whole-ticket quality-review + refactor pass applies to a ledger —\n * the single W610WW trigger shared by BOTH halves of the pass: the cross-scenario\n * refactor row (here, in `validateLedger`) and the `/quality-review` requirement\n * (at the done-gate's skill check). It fires above one RGR loop — `scenarios.length\n * >= 2` AND at least one annotated checkbox. A single-loop ticket has nothing to\n * cross; a pure-legacy ticket (no annotations anywhere) stays exempt regardless of\n * count. Driving both halves from one predicate is what keeps the legacy exemption\n * consistent across the row and the review (and is the \"one derived trigger\" SM1\n * asked for).\n */\nexport function wholeTicketPassApplies(content: string): boolean {\n return wholeTicketPassFromScenarios(parseLedger(content).scenarios);\n}\n\n/**\n * The whole-ticket-pass predicate over already-parsed scenarios — so a caller\n * that has parsed the ledger (validateLedger) doesn't parse it a second time.\n */\nfunction wholeTicketPassFromScenarios(scenarios: ScenarioLedger[]): boolean {\n const hasAnyAnnotation = scenarios.some(s =>\n [s.red, s.green, s.refactor].some(box => box?.annotation),\n );\n return scenarios.length >= 2 && hasAnyAnnotation;\n}\n\nexport function validateLedger(content: string, resolveSha: ShaResolver): LedgerValidationResult {\n const errors: string[] = [];\n const { scenarios, crossScenario } = parseLedger(content);\n\n for (const scenario of scenarios) {\n validateScenario(scenario, resolveSha, errors);\n }\n\n // Cross-scenario row enforcement: required iff the whole-ticket pass applies\n // (≥2 annotated loops — see wholeTicketPassApplies), OR the row already exists\n // (back-compat: a present row is always validated). Pure-legacy and single-loop\n // tickets stay exempt.\n if (wholeTicketPassFromScenarios(scenarios) || crossScenario) {\n validateCrossScenario(crossScenario, resolveSha, errors);\n }\n\n return { ok: errors.length === 0, errors };\n}\n","/**\n * Boundary reconciliation engine (ticket CDRJTW, #810 slice 1-2).\n *\n * Pure composition of safeword's existing evidence checks over the ticket\n * artifacts present in a change. The command layer discovers the change and\n * supplies prior/proposed/at-rest content; this module owns which checks run\n * and how verdicts are shaped. Warn-and-record: verdicts never carry a\n * blocking signal in this slice — hard-block tiers are the server-side child\n * of #810.\n */\n\nimport { checkVerifyArtifact } from '../../templates/hooks/lib/done-gate.js';\nimport { parseImplPlan } from '../../templates/hooks/lib/impl-plan.js';\nimport { type ShaResolver, validateLedger } from '../../templates/hooks/lib/ledger-validation.js';\nimport {\n type ArtifactReader,\n detectUnanchoredPhaseTransition,\n evaluateTicketWrite,\n frontmatterOf as parseTicketFrontmatter,\n type PhaseAnchorScope,\n scalar,\n} from '../../templates/hooks/lib/phase-provenance.js';\n\n/** Ticket-artifact basenames the engine knows how to reconcile. */\nexport const TICKET_ARTIFACTS = new Set([\n 'ticket.md',\n 'test-definitions.md',\n 'verify.md',\n 'impl-plan.md',\n 'spec.md',\n]);\n\nexport interface ChangedArtifact {\n /** Artifact basename, e.g. `ticket.md`. */\n artifact: string;\n /** Content before the change; undefined = artifact is being created. */\n prior?: string;\n /** Content after the change; undefined = artifact is being deleted. */\n proposed?: string;\n}\n\n/** One legality step: a single commit's before/after view of ticket.md. */\nexport interface LegalityStep {\n prior?: string;\n proposed: string;\n /** Short SHA of the commit this step came from, for warning attribution. */\n commit?: string;\n}\n\n/** One touched ticket: its changed artifacts plus at-rest context. */\nexport interface TicketChange {\n /** Canonical ticket ownership and executable-feature-lane scope. */\n anchorScope: PhaseAnchorScope;\n artifacts: ChangedArtifact[];\n /** ticket.md as it stands after the change (staged version, else on-disk). */\n ticketCurrent?: string;\n /** Whether a test-definitions.md exists for this ticket (staged or on disk). */\n hasLedger: boolean;\n /**\n * Per-commit ticket.md transitions for phase legality (N76NQ0). Supplied by\n * the push tier so a multi-commit range that traversed phases one legal step\n * at a time is judged commit-by-commit, never by its endpoints (the CDRJTW\n * closing-push false positive). Absent at commit tier — the endpoint pair IS\n * the single step there.\n */\n legalitySteps?: LegalityStep[];\n}\n\ntype Verdict = 'pass' | 'warn' | 'indeterminate';\n\nexport interface CheckVerdict {\n check: string;\n verdict: Verdict;\n detail?: string;\n}\n\nexport interface TicketReconciliation {\n ticket: string;\n checks: CheckVerdict[];\n}\n\n/** Phases at which a feature's R/G/R ledger must already exist. */\nconst LEDGER_REQUIRED_PHASES = new Set([\n 'scenario-gate',\n 'plan-implementation',\n 'implement',\n 'verify',\n 'done',\n]);\n\n/**\n * Ticket frontmatter, tolerant of CRLF git blobs — the shared parser is\n * LF-only, so normalize before delegating (engine reads raw git content).\n * `scalar` is imported from the same module.\n */\nfunction frontmatterOf(content: string): Record<string, string | string[]> | undefined {\n return parseTicketFrontmatter(content.replaceAll('\\r\\n', '\\n'));\n}\n\nfunction hasEntries(meta: Record<string, string | string[]>, key: string): boolean {\n const value = meta[key];\n return Array.isArray(value) ? value.length > 0 : typeof value === 'string' && value !== '';\n}\n\nfunction pass(check: string): CheckVerdict {\n return { check, verdict: 'pass' };\n}\n\nfunction warnVerdict(check: string, detail: string): CheckVerdict {\n return { check, verdict: 'warn', detail };\n}\n\n/** A check that could not run to a conclusion (oracle failed mid-run) — never a crash. */\nfunction indeterminate(check: string, detail: string): CheckVerdict {\n return { check, verdict: 'indeterminate', detail };\n}\n\n/**\n * Phase legality over the change (N76NQ0): judged per commit when the caller\n * supplies legality steps (push tier — each range commit against its parent),\n * else over the endpoint pair (commit tier, where the pair IS the one step).\n * First illegal step wins, attributed to its commit.\n */\nfunction legalityVerdict(\n ticketFile: ChangedArtifact,\n legalitySteps?: LegalityStep[],\n): CheckVerdict {\n let steps: LegalityStep[] = [];\n if (legalitySteps && legalitySteps.length > 0) {\n steps = legalitySteps;\n } else if (ticketFile.proposed !== undefined) {\n steps = [{ prior: ticketFile.prior, proposed: ticketFile.proposed }];\n }\n for (const step of steps) {\n const verdict = evaluateTicketWrite(step.prior, step.proposed);\n if (!verdict.ok) {\n const at = step.commit === undefined ? '' : ` (commit ${step.commit})`;\n return warnVerdict('phase-legality', `${verdict.reason}${at}`);\n }\n }\n return pass('phase-legality');\n}\n\n/**\n * Anchor check over a ticket.md transition, tree-only via the injected reader\n * (never git history, so both tiers verify fully). A reader failure (git\n * broken mid-run) degrades to an indeterminate verdict, never a crash —\n * exit-0 is absolute.\n */\nfunction phaseAnchorVerdict(\n ticketFile: ChangedArtifact,\n scope: PhaseAnchorScope,\n readArtifact?: ArtifactReader,\n): CheckVerdict {\n try {\n const anchor = detectUnanchoredPhaseTransition(\n ticketFile.prior,\n ticketFile.proposed ?? '',\n scope,\n readArtifact,\n );\n return anchor.kind === 'unanchored'\n ? warnVerdict('phase-anchor', anchor.reason)\n : pass('phase-anchor');\n } catch {\n return indeterminate(\n 'phase-anchor',\n 'artifact read failed mid-run — the anchor could not be determined',\n );\n }\n}\n\n/** Transition checks over a staged/changed ticket.md. */\nfunction ticketFileChecks(\n ticketFile: ChangedArtifact,\n scope: PhaseAnchorScope,\n readArtifact?: ArtifactReader,\n legalitySteps?: LegalityStep[],\n): CheckVerdict[] {\n if (ticketFile.proposed === undefined) return [];\n const checks: CheckVerdict[] = [];\n\n if (frontmatterOf(ticketFile.proposed) === undefined) {\n checks.push(\n warnVerdict(\n 'classify',\n 'ticket.md cannot be classified — its frontmatter is missing or unparseable',\n ),\n );\n }\n\n checks.push(\n legalityVerdict(ticketFile, legalitySteps),\n phaseAnchorVerdict(ticketFile, scope, readArtifact),\n );\n\n return checks;\n}\n\n/**\n * Birth legality for a ticket at rest (#675): a feature past intake whose\n * folder is touched, whose ticket.md is NOT part of the change, and which\n * carries no traversal evidence (no phase_anchors) must justify its birth via\n * phase_skips — the same rule the write-time gate applies to creations.\n */\nfunction atRestBirthCheck(change: TicketChange): CheckVerdict[] {\n if (change.artifacts.some(a => a.artifact === 'ticket.md')) return [];\n if (change.ticketCurrent === undefined) return [];\n const meta = frontmatterOf(change.ticketCurrent);\n if (meta === undefined) return [];\n if (scalar(meta, 'type') !== 'feature') return [];\n const phase = scalar(meta, 'phase');\n if (phase === undefined || phase === 'intake') return [];\n if (hasEntries(meta, 'phase_anchors')) return [pass('birth')];\n\n const birth = evaluateTicketWrite(undefined, change.ticketCurrent);\n return birth.ok\n ? [pass('birth')]\n : [warnVerdict('birth', `ticket at rest looks born past intake: ${birth.reason}`)];\n}\n\n/** The phase at which a changed ticket requires a ledger, or undefined. */\nfunction ledgerRequiredPhase(change: TicketChange): string | undefined {\n if (change.ticketCurrent === undefined) return undefined;\n const meta = frontmatterOf(change.ticketCurrent);\n if (meta === undefined || scalar(meta, 'type') !== 'feature') return undefined;\n const phase = scalar(meta, 'phase');\n return phase !== undefined && LEDGER_REQUIRED_PHASES.has(phase) ? phase : undefined;\n}\n\n/** Ledger presence + format (content tier — reachability is the push tier's job). */\nfunction ledgerChecks(change: TicketChange, resolveSha?: ShaResolver): CheckVerdict[] {\n const checks: CheckVerdict[] = [];\n\n const requiredAt = ledgerRequiredPhase(change);\n if (requiredAt !== undefined && !change.hasLedger) {\n checks.push(\n warnVerdict(\n 'ledger',\n `feature at phase ${requiredAt} has no test-definitions.md — the R/G/R ledger is missing`,\n ),\n );\n }\n\n const ledgerFile = change.artifacts.find(a => a.artifact === 'test-definitions.md');\n if (ledgerFile?.proposed !== undefined) {\n // Content tier treats every SHA as resolvable; the push tier injects the\n // real resolver. A resolver failure degrades to indeterminate, never a crash.\n try {\n const result = validateLedger(ledgerFile.proposed, resolveSha ?? (() => true));\n checks.push(\n result.ok\n ? pass('ledger-format')\n : warnVerdict(\n 'ledger-format',\n `ledger annotation problems: ${result.errors.join(' | ')}`,\n ),\n );\n } catch {\n checks.push(\n indeterminate(\n 'ledger-format',\n 'SHA resolution failed mid-run — ledger reachability could not be determined',\n ),\n );\n }\n }\n\n return checks;\n}\n\n/** Shape checks for verify.md and impl-plan.md when they are part of the change. */\nfunction artifactShapeChecks(change: TicketChange): CheckVerdict[] {\n const checks: CheckVerdict[] = [];\n\n const verifyFile = change.artifacts.find(a => a.artifact === 'verify.md');\n if (verifyFile?.proposed !== undefined) {\n const verdict = checkVerifyArtifact(verifyFile.proposed);\n checks.push(\n verdict.ok\n ? pass('verify-shape')\n : warnVerdict('verify-shape', `verify.md fails its shape check: ${verdict.reason}`),\n );\n }\n\n const planFile = change.artifacts.find(a => a.artifact === 'impl-plan.md');\n if (planFile?.proposed !== undefined) {\n const parsed = parseImplPlan(planFile.proposed);\n checks.push(\n parsed.errors.length === 0\n ? pass('impl-plan-shape')\n : warnVerdict(\n 'impl-plan-shape',\n `impl-plan.md fails its shape check: ${parsed.errors.join(' | ')}`,\n ),\n );\n }\n\n return checks;\n}\n\n/** Checks for one touched ticket: anchors verify through the tree reader,\n * ledger SHAs through the history resolver — deliberately separate oracles. */\nfunction reconcileTicket(\n change: TicketChange,\n resolveSha?: ShaResolver,\n readArtifact?: ArtifactReader,\n): CheckVerdict[] {\n const ticketFile = change.artifacts.find(a => a.artifact === 'ticket.md');\n return [\n ...(ticketFile\n ? ticketFileChecks(ticketFile, change.anchorScope, readArtifact, change.legalitySteps)\n : []),\n ...atRestBirthCheck(change),\n ...ledgerChecks(change, resolveSha),\n ...artifactShapeChecks(change),\n ];\n}\n\n/**\n * Reconcile every ticket touched by a change. Returns one entry per ticket,\n * each carrying its per-check verdicts — pass entries included, so a clean\n * run is a recorded fact, not an absence. `resolveSha` is the LEDGER's\n * history oracle (push tier only — the commit tier stays content-only for\n * SHAs); `readArtifact` is the ANCHOR check's view of the tree being shipped\n * (staged index at commit, HEAD at push) — both tiers verify anchors fully,\n * because a tree read fits the commit tier's sub-second budget.\n */\nexport function reconcileChange(\n changes: TicketChange[],\n resolveSha?: ShaResolver,\n readArtifact?: ArtifactReader,\n): TicketReconciliation[] {\n return changes.map(change => ({\n ticket: change.anchorScope.ticketPath.split('/').at(-1) ?? change.anchorScope.ticketPath,\n checks: reconcileTicket(change, resolveSha, readArtifact),\n }));\n}\n\n/** All findings (non-pass verdicts) across a reconciliation, for warning output. */\nexport function findings(\n reconciliations: TicketReconciliation[],\n): { ticket: string; check: CheckVerdict }[] {\n return reconciliations.flatMap(({ ticket, checks }) =>\n checks.filter(c => c.verdict !== 'pass').map(check => ({ ticket, check })),\n );\n}\n"],"mappings":"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","names":["execFileSync","existsSync","mkdirSync","nodePath","process","existsSync","readFileSync","nodePath","existsSync","readFileSync","statSync","nodePath","nodePath","spawnSync","existsSync","readFileSync","nodePath","frontmatterOf","nodePath","execFileSync","base","existsSync","mkdirSync","process"]}
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalDirectory
|
|
3
|
+
} from "./chunk-S2T356DA.js";
|
|
4
|
+
|
|
1
5
|
// src/claude-plugin/project-root.ts
|
|
2
6
|
import { spawnSync } from "child_process";
|
|
3
|
-
import { realpathSync, statSync } from "fs";
|
|
4
|
-
import nodePath from "path";
|
|
5
|
-
function canonicalDirectory(path) {
|
|
6
|
-
if (typeof path !== "string" || path.trim() === "") return void 0;
|
|
7
|
-
try {
|
|
8
|
-
if (!statSync(path).isDirectory()) return void 0;
|
|
9
|
-
return nodePath.normalize(realpathSync(path));
|
|
10
|
-
} catch {
|
|
11
|
-
return void 0;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
7
|
function canonicalClaudeProjectRoot(cwd) {
|
|
15
8
|
const configuredRoot = process.env.CLAUDE_PROJECT_DIR;
|
|
16
9
|
const environmentRoot = configuredRoot === void 0 ? void 0 : configuredRoot.trim();
|
|
@@ -34,4 +27,4 @@ function canonicalClaudeProjectRoot(cwd) {
|
|
|
34
27
|
export {
|
|
35
28
|
canonicalClaudeProjectRoot
|
|
36
29
|
};
|
|
37
|
-
//# sourceMappingURL=chunk-
|
|
30
|
+
//# sourceMappingURL=chunk-34NCOTP5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/claude-plugin/project-root.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\n\nimport { canonicalDirectory } from '../utils/fs.js';\n\nexport function canonicalClaudeProjectRoot(cwd: string): string {\n const configuredRoot = process.env.CLAUDE_PROJECT_DIR;\n const environmentRoot = configuredRoot === undefined ? undefined : configuredRoot.trim();\n let candidate = environmentRoot === '' ? undefined : environmentRoot;\n if (candidate === undefined) {\n const result = spawnSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {\n encoding: 'utf8',\n });\n const gitRoot = result.status === 0 ? result.stdout?.trim() : undefined;\n candidate = gitRoot === '' || gitRoot === undefined ? cwd : gitRoot;\n }\n const canonical = canonicalDirectory(candidate);\n if (canonical === undefined) {\n throw new Error(\n `Claude project root is missing, not a directory, or cannot be resolved: ${candidate}`,\n );\n }\n return canonical;\n}\n"],"mappings":";;;;;AAAA,SAAS,iBAAiB;AAInB,SAAS,2BAA2B,KAAqB;AAC9D,QAAM,iBAAiB,QAAQ,IAAI;AACnC,QAAM,kBAAkB,mBAAmB,SAAY,SAAY,eAAe,KAAK;AACvF,MAAI,YAAY,oBAAoB,KAAK,SAAY;AACrD,MAAI,cAAc,QAAW;AAC3B,UAAM,SAAS,UAAU,OAAO,CAAC,MAAM,KAAK,aAAa,iBAAiB,GAAG;AAAA,MAC3E,UAAU;AAAA,IACZ,CAAC;AACD,UAAM,UAAU,OAAO,WAAW,IAAI,OAAO,QAAQ,KAAK,IAAI;AAC9D,gBAAY,YAAY,MAAM,YAAY,SAAY,MAAM;AAAA,EAC9D;AACA,QAAM,YAAY,mBAAmB,SAAS;AAC9C,MAAI,cAAc,QAAW;AAC3B,UAAM,IAAI;AAAA,MACR,2EAA2E,SAAS;AAAA,IACtF;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
isDirectory,
|
|
5
5
|
readFileSafe,
|
|
6
6
|
readJson
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-S2T356DA.js";
|
|
8
8
|
|
|
9
9
|
// src/utils/architecture-monorepo.ts
|
|
10
10
|
import { createHash } from "crypto";
|
|
@@ -875,4 +875,4 @@ export {
|
|
|
875
875
|
extractMonorepoArchitectureSnapshot,
|
|
876
876
|
monorepoFingerprintOf
|
|
877
877
|
};
|
|
878
|
-
//# sourceMappingURL=chunk-
|
|
878
|
+
//# sourceMappingURL=chunk-3EJNDIAO.js.map
|
|
@@ -3,7 +3,10 @@ import {
|
|
|
3
3
|
observeCursorProject,
|
|
4
4
|
projectLifecycleSchema,
|
|
5
5
|
unselectedCursorFinding
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-RXQJH4LM.js";
|
|
7
|
+
import {
|
|
8
|
+
detectPackageManager
|
|
9
|
+
} from "./chunk-6S7CTKGC.js";
|
|
7
10
|
import {
|
|
8
11
|
legacyGlobalGuidanceDiagnostic,
|
|
9
12
|
observeLegacyGlobalGuidance
|
|
@@ -12,9 +15,6 @@ import {
|
|
|
12
15
|
compareVersions,
|
|
13
16
|
isSafePackageVersion
|
|
14
17
|
} from "./chunk-L6XMSCXJ.js";
|
|
15
|
-
import {
|
|
16
|
-
detectPackageManager
|
|
17
|
-
} from "./chunk-6S7CTKGC.js";
|
|
18
18
|
import {
|
|
19
19
|
DEFAULT_AGENT_INTEGRATIONS
|
|
20
20
|
} from "./chunk-23BNG6FE.js";
|
|
@@ -78,11 +78,11 @@ async function observeLifecycleSurfaces(cwd, agents, environment = process.env)
|
|
|
78
78
|
const project = await observeStatus(cwd, agents, environment);
|
|
79
79
|
const surfaces = [{ name: "project", result: project }];
|
|
80
80
|
if (agents.includes("claude")) {
|
|
81
|
-
const { observeClaudeStatus } = await import("./status-
|
|
81
|
+
const { observeClaudeStatus } = await import("./status-OFXGT5ZO.js");
|
|
82
82
|
surfaces.push({ name: "claude", result: observeClaudeStatus(cwd) });
|
|
83
83
|
}
|
|
84
84
|
if (agents.includes("codex")) {
|
|
85
|
-
const { observeCodexMigration } = await import("./operations-
|
|
85
|
+
const { observeCodexMigration } = await import("./operations-5CJI6U6T.js");
|
|
86
86
|
surfaces.push({ name: "codex", result: observeCodexMigration(cwd, environment) });
|
|
87
87
|
}
|
|
88
88
|
if (agents.includes("cursor")) {
|
|
@@ -176,6 +176,11 @@ async function observeProjectStatus(cwd, agents) {
|
|
|
176
176
|
"MISSING_PACKAGE",
|
|
177
177
|
"error"
|
|
178
178
|
),
|
|
179
|
+
...healthFindings(
|
|
180
|
+
health.missingPythonTools.map((tool) => `${tool} is not declared for this Python project.`),
|
|
181
|
+
"MISSING_PYTHON_TOOL",
|
|
182
|
+
"error"
|
|
183
|
+
),
|
|
179
184
|
...healthFindings(health.issues, "PROJECT_DRIFT", "warning")
|
|
180
185
|
];
|
|
181
186
|
const versionGuidance = projectVersionFinding(cwd, health.projectVersion, health.cliVersion);
|
|
@@ -233,4 +238,4 @@ export {
|
|
|
233
238
|
summarizeLifecycleStatus,
|
|
234
239
|
observeLifecycleStatus
|
|
235
240
|
};
|
|
236
|
-
//# sourceMappingURL=chunk-
|
|
241
|
+
//# sourceMappingURL=chunk-3ETWYWGQ.js.map
|