devflow-kit 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/README.md +111 -18
  3. package/dist/agents/git.md +822 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/ambient.js +160 -145
  6. package/dist/cli/commands/attribution-prompts.js +1 -1
  7. package/dist/cli/commands/capture.js +29 -55
  8. package/dist/cli/commands/compliance-prompts.js +1 -1
  9. package/dist/cli/commands/compliance.js +48 -55
  10. package/dist/cli/commands/context.js +17 -32
  11. package/dist/cli/commands/debug.js +65 -26
  12. package/dist/cli/commands/flags.js +3 -3
  13. package/dist/cli/commands/hud.js +34 -10
  14. package/dist/cli/commands/init-seed.js +61 -27
  15. package/dist/cli/commands/init.js +649 -240
  16. package/dist/cli/commands/install-report.js +200 -0
  17. package/dist/cli/commands/knowledge/index.js +2 -2
  18. package/dist/cli/commands/knowledge/toggle.js +35 -37
  19. package/dist/cli/commands/learning.js +79 -57
  20. package/dist/cli/commands/legacy-hooks.js +11 -14
  21. package/dist/cli/commands/memory.js +134 -135
  22. package/dist/cli/commands/prompt-io.js +4 -4
  23. package/dist/cli/commands/proxy.js +23 -41
  24. package/dist/cli/commands/security.js +81 -29
  25. package/dist/cli/commands/skills.js +71 -7
  26. package/dist/cli/commands/tracker-prompts.js +145 -0
  27. package/dist/cli/commands/tracker.js +277 -0
  28. package/dist/cli/commands/uninstall.js +520 -169
  29. package/dist/cli.js +2 -0
  30. package/dist/commands/bug-analysis.md +58 -14
  31. package/dist/commands/code-review.md +110 -32
  32. package/dist/commands/debug.md +55 -11
  33. package/dist/commands/dynamic-build.md +344 -73
  34. package/dist/commands/dynamic-plan.md +77 -27
  35. package/dist/commands/dynamic-profile.md +25 -11
  36. package/dist/commands/dynamic-tickets.md +76 -15
  37. package/dist/commands/explore.md +37 -7
  38. package/dist/commands/implement.md +314 -62
  39. package/dist/commands/plan.md +146 -32
  40. package/dist/commands/release.md +64 -17
  41. package/dist/commands/research.md +34 -8
  42. package/dist/commands/resolve.md +196 -68
  43. package/dist/commands/self-review.md +45 -9
  44. package/dist/core/agent-models.js +55 -12
  45. package/dist/core/assets.js +58 -2
  46. package/dist/core/compliance-compose.js +27 -27
  47. package/dist/core/evidence-policy.js +363 -0
  48. package/dist/core/feature-config.js +200 -65
  49. package/dist/core/feature-switch.js +112 -0
  50. package/dist/core/flags.js +34 -6
  51. package/dist/core/fs-atomic.js +27 -0
  52. package/dist/core/hook-log-dirs.js +104 -0
  53. package/dist/core/learning-tuning-config.js +5 -3
  54. package/dist/core/ledger-root.js +102 -0
  55. package/dist/core/manifest.js +38 -10
  56. package/dist/core/mds-variants.js +798 -0
  57. package/dist/core/migrations.js +49 -23
  58. package/dist/core/model-discovery.js +12 -1
  59. package/dist/core/plugins.js +361 -12
  60. package/dist/core/project-paths.js +1 -18
  61. package/dist/core/proxy-log.js +8 -6
  62. package/dist/core/proxy-state.js +11 -8
  63. package/dist/core/reference-sweep.js +136 -0
  64. package/dist/core/same-location.js +25 -0
  65. package/dist/core/tracker.js +494 -0
  66. package/dist/hud/components/config-counts.js +15 -4
  67. package/dist/hud/components/learning-counts.js +14 -0
  68. package/dist/hud/config.js +2 -1
  69. package/dist/hud/cost-history.js +2 -4
  70. package/dist/hud/git.js +52 -7
  71. package/dist/hud/index.js +7 -9
  72. package/dist/skills/git/references/decision-markers.md +19 -0
  73. package/dist/skills/git/references/learn-conventions.md +56 -0
  74. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  75. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  76. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  77. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  78. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  79. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  80. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  81. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  82. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  83. package/dist/skills/git/references/publication-gate.md +13 -0
  84. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  85. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  87. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  89. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  90. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  91. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  92. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  93. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  94. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  95. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  96. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  97. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  98. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  99. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  100. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  101. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  102. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  103. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  104. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  105. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  106. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  107. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  108. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  109. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  110. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  111. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  112. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  113. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  114. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  115. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  116. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  117. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  118. package/dist/skills/git/references/trust-rule.md +7 -0
  119. package/dist/targets/claude-code/claude-paths.js +59 -57
  120. package/dist/targets/claude-code/compliance-install.js +49 -65
  121. package/dist/targets/claude-code/hooks.js +108 -3
  122. package/dist/targets/claude-code/installer.js +1187 -32
  123. package/dist/targets/claude-code/legacy.js +5 -0
  124. package/dist/targets/claude-code/post-install.js +366 -151
  125. package/dist/targets/claude-code/tracker-install.js +134 -0
  126. package/package.json +8 -6
  127. package/src/assets/agents/code.md +45 -6
  128. package/src/assets/agents/design.md +2 -1
  129. package/src/assets/agents/git.mds +825 -0
  130. package/src/assets/agents/knowledge.md +3 -3
  131. package/src/assets/agents/learning.md +11 -0
  132. package/src/assets/agents/review.md +3 -1
  133. package/src/assets/agents/synthesize.md +1 -1
  134. package/src/assets/agents/test.md +16 -5
  135. package/src/assets/agents/tracker.md +474 -0
  136. package/src/assets/agents/validate.md +7 -5
  137. package/src/assets/commands/_partials/_compliance.mds +19 -1
  138. package/src/assets/commands/_partials/_decisions.mds +15 -3
  139. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  140. package/src/assets/commands/_partials/_engine.mds +13 -11
  141. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  142. package/src/assets/commands/_partials/_factory.mds +1 -1
  143. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  144. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  145. package/src/assets/commands/_partials/_preamble.mds +2 -2
  146. package/src/assets/commands/_partials/_publication.mds +8 -2
  147. package/src/assets/commands/_partials/_settings.mds +28 -0
  148. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  149. package/src/assets/commands/_partials/_tracker.mds +18 -0
  150. package/src/assets/commands/_partials/_wave.mds +16 -10
  151. package/src/assets/commands/bug-analysis.mds +31 -19
  152. package/src/assets/commands/code-review.mds +67 -41
  153. package/src/assets/commands/debug.mds +13 -7
  154. package/src/assets/commands/dynamic-build.mds +274 -66
  155. package/src/assets/commands/dynamic-plan.mds +50 -23
  156. package/src/assets/commands/dynamic-profile.mds +24 -11
  157. package/src/assets/commands/dynamic-tickets.mds +63 -16
  158. package/src/assets/commands/explore.mds +4 -5
  159. package/src/assets/commands/implement.mds +234 -67
  160. package/src/assets/commands/plan.mds +91 -33
  161. package/src/assets/commands/release.md +64 -17
  162. package/src/assets/commands/research.mds +11 -9
  163. package/src/assets/commands/resolve.mds +150 -78
  164. package/src/assets/commands/self-review.mds +24 -25
  165. package/src/assets/mds/git/_pr.mds +331 -0
  166. package/src/assets/mds/git/_references.mds +135 -0
  167. package/src/assets/mds/tracker/_common.mds +156 -0
  168. package/src/assets/mds/tracker/_github.mds +472 -0
  169. package/src/assets/mds/tracker/_jira.mds +407 -0
  170. package/src/assets/mds/tracker/_linear.mds +449 -0
  171. package/src/assets/mds/tracker/_mcp.mds +305 -0
  172. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  173. package/src/assets/scripts/hooks/background-memory-update +40 -19
  174. package/src/assets/scripts/hooks/capture-prompt +18 -8
  175. package/src/assets/scripts/hooks/capture-question +18 -8
  176. package/src/assets/scripts/hooks/capture-turn +27 -13
  177. package/src/assets/scripts/hooks/debug-trace +11 -6
  178. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  179. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  180. package/src/assets/scripts/hooks/ensure-root-gitignore +236 -60
  181. package/src/assets/scripts/hooks/git-marker +48 -0
  182. package/src/assets/scripts/hooks/hook-log-init +3 -1
  183. package/src/assets/scripts/hooks/json-helper.cjs +228 -5
  184. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
  185. package/src/assets/scripts/hooks/log-paths +80 -0
  186. package/src/assets/scripts/hooks/memory-worker +22 -13
  187. package/src/assets/scripts/hooks/pre-compact-memory +44 -15
  188. package/src/assets/scripts/hooks/preamble +1 -4
  189. package/src/assets/scripts/hooks/queue-append +146 -28
  190. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  191. package/src/assets/scripts/hooks/session-start-context +534 -20
  192. package/src/assets/scripts/hooks/session-start-memory +38 -15
  193. package/src/assets/scripts/lib/project-config.cjs +633 -0
  194. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  195. package/src/assets/scripts/redact-secrets.cjs +490 -62
  196. package/src/assets/scripts/release-trace.cjs +1143 -0
  197. package/src/assets/scripts/resolve-evidence-policy.cjs +1145 -0
  198. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  199. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  200. package/src/assets/skills/compliance/SKILL.md +4 -2
  201. package/src/assets/skills/docs-framework/SKILL.md +11 -10
  202. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  203. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  204. package/src/assets/skills/git/SKILL.md +8 -78
  205. package/src/assets/skills/git/references/github-api.md +179 -141
  206. package/src/assets/skills/git/references/patterns.md +11 -6
  207. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  208. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  209. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  210. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  211. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  212. package/src/targets/claude-code/templates/managed-settings.json +25 -9
  213. package/src/assets/agents/git.md +0 -938
@@ -0,0 +1,1145 @@
1
+ #!/usr/bin/env node
2
+ // src/assets/scripts/resolve-evidence-policy.cjs
3
+ //
4
+ // Resolves EVIDENCE_POLICY for a repository and prints it on ONE line together
5
+ // with the three mechanism inputs that operations receive. Installed as a
6
+ // top-level sibling of hud.sh and redact-secrets.cjs under ~/.devflow/scripts/,
7
+ // with its parser beside it in lib/project-config.cjs.
8
+ //
9
+ // Usage: node resolve-evidence-policy.cjs [<dir>] (<dir> defaults to cwd)
10
+ //
11
+ // The policy is plumbing, decided once by the caller: this script prints it plus
12
+ // the mechanism inputs, and operations only ever see the inputs. It WRITES NOTHING
13
+ // — no file, no git ref, no remote state — so `.devflow/project.json` stays a
14
+ // team-owned file that only the team commits (applies ADR-024). A committed
15
+ // `.devflow/policy.json` is detected by presence and never parsed
16
+ // (D-POLICY-JSON-RETIRED).
17
+ //
18
+ // stdout is exactly one line plus "\n", or empty (D-POLICY-LINE):
19
+ // EVIDENCE_POLICY=<required|standard> SOURCE=<file|worktree|default|invalid|error>
20
+ // REF=<branch|none>[ WARN=<w>[,<w>…]] ISSUE_REQUIRED=<bool> APPLY_CONVENTIONS=<bool>
21
+ // REQUIRE_NON_AUTHOR_APPROVAL=<bool>
22
+ // (one line on stdout; wrapped here for reading). Every value is a token from a
23
+ // closed vocabulary or a SAFE_REF_RE-checked branch name, so no byte of a policy
24
+ // file, a gh answer or a git answer can reach stdout.
25
+ //
26
+ // Exit codes (a caller treats EVERY non-zero code as `required`):
27
+ // 0 resolved — the line above
28
+ // 1 usage error — stdout entirely empty, usage on stderr
29
+ // 2 input unusable — <dir> missing or not a directory; prints FAIL_CLOSED_LINE
30
+ // 3 never emitted — this script writes no file, so the write-failure code of
31
+ // redact-secrets.cjs has no arm here and is absent from EXIT_CODES
32
+ // 4 internal error — or git could not say whether <dir> is in a repository
33
+ // (missing, timed out, killed); prints FAIL_CLOSED_LINE
34
+ // 5 output gate refused — the composed line failed the grammar or was not
35
+ // consistent with the resolved policy; prints FAIL_CLOSED_LINE. Final: a
36
+ // re-run resolves the same inputs to the same refusal
37
+ //
38
+ // Design constraints (binding):
39
+ // - main() returns {code, line} and never calls process.exit; the single
40
+ // `require.main === module` boundary is the only stdout write and the only
41
+ // exitCode assignment, so nothing is truncated and no cleanup is skipped
42
+ // - every subprocess is spawned with an argv array (never a shell), stdin
43
+ // ignored, a timeout and a maxBuffer; every loop has a fixed bound
44
+ // - a project.json that is not a regular file is never opened, and one over
45
+ // MAX_CONFIG_BYTES is never read; a working-tree policy.json is never opened
46
+ // - no index-refreshing git command runs (no `status`, no `diff`), so a
47
+ // repository's configured fsmonitor hook never fires; no fetch, no set-head
48
+
49
+ 'use strict';
50
+
51
+ const fs = require('fs');
52
+ const path = require('path');
53
+ const childProcess = require('child_process');
54
+ const projectConfig = require('./lib/project-config.cjs');
55
+
56
+ /** Largest project.json read, in bytes — the shared parser's bound. */
57
+ const { MAX_CONFIG_BYTES } = projectConfig;
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Closed vocabularies
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /** @typedef {'required' | 'standard'} Policy */
64
+ /** @typedef {'file' | 'worktree' | 'default' | 'invalid' | 'error'} Source */
65
+ /** @typedef {'remote-unavailable' | 'invalid-file' | 'raised-by-compliance' | 'pr-changes-policy'} Warning */
66
+
67
+ /** Every policy value, strictest first. */
68
+ const POLICIES = Object.freeze(/** @type {Policy[]} */ (['required', 'standard']));
69
+
70
+ /** Every SOURCE value — the governing input, or `error` for a fail-closed exit. */
71
+ const SOURCES = Object.freeze(/** @type {Source[]} */ (['file', 'worktree', 'default', 'invalid', 'error']));
72
+
73
+ /**
74
+ * Every WARN token, in the order the line prints them.
75
+ * remote-unavailable the default branch's file could not be consulted
76
+ * invalid-file a folded source (remote, worktree or tracking copy) was invalid —
77
+ * its project.json evidence, or the retired policy.json's presence
78
+ * raised-by-compliance compliance raised a `standard` file or worktree policy
79
+ * pr-changes-policy HEAD or the worktree differs from the default branch (advisory)
80
+ */
81
+ const WARNINGS = Object.freeze(/** @type {Warning[]} */ ([
82
+ 'remote-unavailable',
83
+ 'invalid-file',
84
+ 'raised-by-compliance',
85
+ 'pr-changes-policy',
86
+ ]));
87
+
88
+ /**
89
+ * Exit codes by meaning. 3 is deliberately absent: no arm of this script can
90
+ * produce it, and a value in a closed vocabulary that no arm reaches reads as a
91
+ * live outcome to anyone auditing the set.
92
+ */
93
+ const EXIT_CODES = Object.freeze({
94
+ RESOLVED: 0,
95
+ USAGE: 1,
96
+ INPUT_UNUSABLE: 2,
97
+ INTERNAL_ERROR: 4,
98
+ OUTPUT_GATE_REFUSED: 5,
99
+ });
100
+
101
+ /**
102
+ * @typedef {{ ISSUE_REQUIRED: boolean, APPLY_CONVENTIONS: boolean, REQUIRE_NON_AUTHOR_APPROVAL: boolean }} MechanismInputs
103
+ */
104
+
105
+ /**
106
+ * D-POLICY-PLUMBING: the mechanism inputs are a pure function of EVIDENCE_POLICY,
107
+ * and this table is their single authority. Operations receive these three
108
+ * booleans and never the policy itself; a prompt that restated the mapping would
109
+ * be a second authority free to drift from this one.
110
+ * ISSUE_REQUIRED a tracked issue is mandatory for the task
111
+ * APPLY_CONVENTIONS learned naming conventions are applied (branch, PR title)
112
+ * REQUIRE_NON_AUTHOR_APPROVAL merge-readiness demands a non-author approval
113
+ * Per-PR content (a PR's test-plan block, its exceptions) is rendered by callers
114
+ * and is not a function of the policy, so it is not emitted here.
115
+ *
116
+ * @type {Readonly<Record<Policy, Readonly<MechanismInputs>>>}
117
+ */
118
+ const MECHANISM_INPUTS = Object.freeze({
119
+ required: Object.freeze({ ISSUE_REQUIRED: true, APPLY_CONVENTIONS: true, REQUIRE_NON_AUTHOR_APPROVAL: true }),
120
+ standard: Object.freeze({ ISSUE_REQUIRED: false, APPLY_CONVENTIONS: false, REQUIRE_NON_AUTHOR_APPROVAL: false }),
121
+ });
122
+
123
+ /** The mechanism-input keys, in line order. */
124
+ const INPUT_KEYS = Object.freeze(['ISSUE_REQUIRED', 'APPLY_CONVENTIONS', 'REQUIRE_NON_AUTHOR_APPROVAL']);
125
+
126
+ /**
127
+ * A branch name this script will put into argv or onto stdout — the shared gate
128
+ * (lib/project-config.cjs), so the settings resolver reads the same branch names.
129
+ */
130
+ const { SAFE_REF_RE } = projectConfig;
131
+
132
+ /**
133
+ * D-POLICY-LINE: the exported output grammar — anchored, closed alternations
134
+ * only, named groups for consumers. Consumers' parses are pinned to this
135
+ * expression, so it is the one definition of a well-formed line. REF admits
136
+ * `none` or a SAFE_REF_RE branch (a branch literally named `none` is ambiguous;
137
+ * REF is display-only and no consumer branches on it).
138
+ */
139
+ const OUTPUT_LINE_RE = /^EVIDENCE_POLICY=(?<policy>required|standard) SOURCE=(?<source>file|worktree|default|invalid|error) REF=(?<ref>none|(?![A-Za-z0-9._/-]{0,254}\.\.)[A-Za-z0-9][A-Za-z0-9._/-]{0,254})(?: WARN=(?<warn>(?:remote-unavailable|invalid-file|raised-by-compliance|pr-changes-policy)(?:,(?:remote-unavailable|invalid-file|raised-by-compliance|pr-changes-policy)){0,3}))? ISSUE_REQUIRED=(?<issue>true|false) APPLY_CONVENTIONS=(?<conventions>true|false) REQUIRE_NON_AUTHOR_APPROVAL=(?<approval>true|false)$/;
140
+
141
+ /**
142
+ * The line every refusal prints (exits 2, 4 and 5). A constant, so the boundary
143
+ * can never fail to compose it, and `required` in every field a consumer reads.
144
+ */
145
+ const FAIL_CLOSED_LINE =
146
+ 'EVIDENCE_POLICY=required SOURCE=error REF=none ISSUE_REQUIRED=true APPLY_CONVENTIONS=true REQUIRE_NON_AUTHOR_APPROVAL=true';
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Subprocess bounds (every spawn carries a timeout and a maxBuffer)
150
+ // ---------------------------------------------------------------------------
151
+
152
+ const GH_TIMEOUT_MS = 10000;
153
+ const GIT_REMOTE_TIMEOUT_MS = 10000;
154
+ const GIT_LOCAL_TIMEOUT_MS = 5000;
155
+ /** gh answers and blob reads: generous for a 4 KiB project.json, and an overflow is ENOBUFS ⇒ invalid. */
156
+ const BLOB_MAX_BUFFER = 65536;
157
+ /** rev-parse and ls-remote print one short line each. */
158
+ const LINE_MAX_BUFFER = 4096;
159
+
160
+ /** The repository-relative path of the retired policy file, in git and API spelling (D-POLICY-JSON-RETIRED). */
161
+ const POLICY_REL = '.devflow/policy.json';
162
+
163
+ /** The repository-relative path of the team config file, in git and API spelling. */
164
+ const PROJECT_REL = '.devflow/project.json';
165
+
166
+ /** `git ls-remote --symref origin HEAD`, first line. Bounded class, no backtracking. */
167
+ const LS_REMOTE_SYMREF_RE = /^ref: refs\/heads\/([^\t\n]{1,255})\tHEAD$/;
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Types shared by the helpers
171
+ // ---------------------------------------------------------------------------
172
+
173
+ /**
174
+ * @typedef {{ kind: 'absent' } | { kind: 'invalid' } | { kind: 'valid', policy: Policy }} ParsedPolicy
175
+ *
176
+ * @typedef {{ policy: Policy, source: Source, ref: string, warnings: Warning[], inputs: MechanismInputs }} Resolution
177
+ * `ref` is the default branch as resolved, else `none`. Frozen, arrays included.
178
+ *
179
+ * @typedef {{ status: number | null, stdout?: Buffer | string, stderr?: Buffer | string, error?: { code?: string } }} ExecResult
180
+ * The spawnSync subset this script reads.
181
+ *
182
+ * @typedef {(file: string, args: string[], opts: object) => ExecResult} ExecFn
183
+ * Called exactly like child_process.spawnSync(file, args, opts).
184
+ *
185
+ * @typedef {{ dir: string, compliance?: unknown }} ResolveOptions
186
+ * `compliance` is the caller's already-read `manifest.features.compliance` value
187
+ * (raw or normalized — complianceDefault accepts either). Omitted (undefined), the
188
+ * script reads the manifest itself; pass `null` for "no compliance state".
189
+ *
190
+ * @typedef {{ exec?: ExecFn }} ResolveDeps
191
+ * `exec` is the only injected I/O; it defaults to child_process.spawnSync.
192
+ *
193
+ * @typedef {{ kind: 'resolve', dir: string } | { kind: 'usage', usage: string }} ParsedArgs
194
+ *
195
+ * @typedef {{ exec?: ExecFn, formatLine?: (r: Resolution) => unknown }} MainDeps
196
+ * Injected by tests only, to reach the output gate.
197
+ *
198
+ * @typedef {{ code: number, line: string }} MainOutcome
199
+ * `line` is '' only for code 1, FAIL_CLOSED_LINE for codes 2, 4 and 5.
200
+ */
201
+
202
+ /** @type {ParsedPolicy} */
203
+ const ABSENT = Object.freeze({ kind: 'absent' });
204
+ /** @type {ParsedPolicy} */
205
+ const INVALID = Object.freeze({ kind: 'invalid' });
206
+
207
+ /** @type {Resolution} */
208
+ const ERROR_RESOLUTION = Object.freeze({
209
+ policy: 'required',
210
+ source: 'error',
211
+ ref: 'none',
212
+ warnings: Object.freeze([]),
213
+ inputs: MECHANISM_INPUTS.required,
214
+ });
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // parseArgs
218
+ // ---------------------------------------------------------------------------
219
+
220
+ /**
221
+ * Parse argv into a directory or a usage error. `kind` is the discriminant, and
222
+ * main() dispatches on it alone. Any `-`-prefixed argument (including `-` and
223
+ * `--`) or a second positional is a usage error, never an ignored argument.
224
+ *
225
+ * @param {readonly string[]} argv process.argv
226
+ * @returns {ParsedArgs}
227
+ */
228
+ function parseArgs(argv) {
229
+ const USAGE = 'Usage: node resolve-evidence-policy.cjs [<dir>]';
230
+ const rest = Array.isArray(argv) ? argv.slice(2) : [];
231
+ /** @type {string[]} */
232
+ const positionals = [];
233
+ for (const arg of rest) {
234
+ if (typeof arg !== 'string' || arg.startsWith('-')) {
235
+ return {
236
+ kind: 'usage',
237
+ usage: 'resolve-evidence-policy: unrecognised argument ' + JSON.stringify(String(arg)).slice(0, 80) + '\n' + USAGE,
238
+ };
239
+ }
240
+ positionals.push(arg);
241
+ }
242
+ if (positionals.length > 1) return { kind: 'usage', usage: USAGE };
243
+ return { kind: 'resolve', dir: positionals.length === 1 ? positionals[0] : process.cwd() };
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // Compliance default
248
+ // ---------------------------------------------------------------------------
249
+
250
+ /**
251
+ * The compliance default C. Mirrors the CLI's `normalizeComplianceFeature`
252
+ * (src/core/compliance.ts) exactly — absent or malformed is disabled — behind a
253
+ * parity test. Enabled is `required` WHATEVER the framework count: an install
254
+ * with zero frameworks ("generic controls only") still installs the compliance
255
+ * skill and is gated today, and no compliance-on user may lose that. Which
256
+ * frameworks are listed therefore never matters, only that the value is
257
+ * well-formed.
258
+ *
259
+ * @param {unknown} rawFeatureValue `manifest.features.compliance`, raw or normalized
260
+ * @returns {Policy}
261
+ */
262
+ function complianceDefault(rawFeatureValue) {
263
+ const raw = /** @type {any} */ (rawFeatureValue);
264
+ if (raw === null || raw === undefined || typeof raw !== 'object' || Array.isArray(raw)) return 'standard';
265
+ if (typeof raw.enabled !== 'boolean') return 'standard';
266
+ if (!Array.isArray(raw.frameworks)) return 'standard';
267
+ if (!raw.frameworks.every((/** @type {unknown} */ f) => typeof f === 'string')) return 'standard';
268
+ return raw.enabled ? 'required' : 'standard';
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Bounded file reads
273
+ // ---------------------------------------------------------------------------
274
+
275
+ /** Bounded, never-writing file read — the shared one (lib/project-config.cjs). */
276
+ const readBoundedRegularFile = projectConfig.readBoundedRegularFile;
277
+
278
+ /**
279
+ * D-POLICY-JSON-RETIRED: `.devflow/policy.json` is never parsed. At a source whose
280
+ * project.json does not decide the evidence (absent, or present without an
281
+ * `evidence` key), an existing policy.json makes that source `invalid` — whatever
282
+ * its bytes say, `standard` and unreadable alike — so the repository resolves
283
+ * `required` with `invalid-file` until the team states its value as `evidence` in
284
+ * project.json. With neither file the source is absent. Presence is the whole
285
+ * signal, and each source probes it with the call it already makes:
286
+ * R the default branch's contents call — exit 0 or an overflow is present, a
287
+ * 404 absent, anything else the remote unavailable (remoteSource)
288
+ * T/H `git cat-file blob` — exit 0 present, an answered non-zero exit absent,
289
+ * an unanswered call invalid (catFilePolicyPresence)
290
+ * W an lstat that never opens the file — ENOENT or ENOTDIR absent, anything
291
+ * else present, a symlink or FIFO included (worktreePolicyPresence)
292
+ * The bytes a call returns are discarded unread, so no value in the file can
293
+ * lower the policy or reach stdout.
294
+ *
295
+ * @param {boolean} present
296
+ * @returns {ParsedPolicy}
297
+ */
298
+ function retiredPolicyReading(present) {
299
+ return present ? INVALID : ABSENT;
300
+ }
301
+
302
+ /**
303
+ * W — whether the working tree holds a policy.json. lstat only: the file is never
304
+ * opened, so a FIFO cannot block and a symlink is never followed. An lstat that
305
+ * fails for any reason but "no such path" cannot prove absence, so it reads as
306
+ * present (fail closed).
307
+ *
308
+ * @param {string} root
309
+ * @returns {ParsedPolicy}
310
+ */
311
+ function worktreePolicyPresence(root) {
312
+ try {
313
+ fs.lstatSync(path.join(root, '.devflow', 'policy.json'));
314
+ return retiredPolicyReading(true);
315
+ } catch (/** @type {any} */ err) {
316
+ return retiredPolicyReading(!(err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')));
317
+ }
318
+ }
319
+
320
+ // ---------------------------------------------------------------------------
321
+ // Source readings (D-POLICY-SOURCE-PRECEDENCE, D-COMPLIANCE-REPO-FLOOR)
322
+ // ---------------------------------------------------------------------------
323
+
324
+ /**
325
+ * @typedef {{ evidence: ParsedPolicy, compliance: boolean }} SourceReading
326
+ * What one source (R, T, H or W) says: its evidence policy, and whether its
327
+ * project.json declares repository compliance.
328
+ *
329
+ * @typedef {{ evidence: ParsedPolicy | null, compliance: boolean }} ProjectReading
330
+ * `evidence` null ⇒ project.json does not decide it, so the presence of the
331
+ * retired policy file at the same source does (D-POLICY-JSON-RETIRED).
332
+ *
333
+ * @typedef {{ kind: 'absent' } | { kind: 'invalid' } | { kind: 'parsed', evidence: { kind: string, value?: unknown }, compliance: { kind: string } }} ProjectConfigView
334
+ * The part of lib/project-config.cjs ProjectConfig this script reads.
335
+ */
336
+
337
+ /** @type {ProjectConfigView} */
338
+ const PROJECT_ABSENT = Object.freeze({ kind: 'absent' });
339
+ /** @type {ProjectConfigView} */
340
+ const PROJECT_INVALID = Object.freeze({ kind: 'invalid' });
341
+
342
+ /**
343
+ * D-POLICY-SOURCE-PRECEDENCE: at every source, the `evidence` key of
344
+ * `project.json` is the one authority on the evidence policy. A valid value
345
+ * decides it, and a present-but-malformed or duplicated `evidence` decides
346
+ * `invalid` (⇒ required); a project.json that is not a JSON object at all is
347
+ * invalid outright. Only where project.json is absent, or present without an
348
+ * `evidence` key, does the same source's retired `policy.json` count — by
349
+ * presence alone (D-POLICY-JSON-RETIRED). The precedence is per source, so each
350
+ * source says ONE thing and foldPolicy folds them unchanged.
351
+ *
352
+ * D-COMPLIANCE-REPO-FLOOR: a `compliance` key in project.json at R, T or W — any
353
+ * value, an empty list and a malformed one included — makes the repository's
354
+ * compliance default `required`, folded with the machine's as
355
+ * stricter(C_machine, C_repo). A machine with compliance on at zero frameworks is
356
+ * already `required`, and a repository that declares compliance is held to the
357
+ * same floor. A project.json that cannot be read counts as declaring it (fail
358
+ * closed).
359
+ *
360
+ * @param {ProjectConfigView} project
361
+ * @returns {ProjectReading}
362
+ */
363
+ function projectReading(project) {
364
+ if (project.kind === 'absent') return { evidence: null, compliance: false };
365
+ if (project.kind === 'invalid') return { evidence: INVALID, compliance: true };
366
+ const compliance = project.compliance.kind !== 'absent';
367
+ const evidence = project.evidence;
368
+ if (evidence.kind === 'valid' && POLICIES.includes(/** @type {Policy} */ (evidence.value))) {
369
+ return { evidence: Object.freeze({ kind: 'valid', policy: /** @type {Policy} */ (evidence.value) }), compliance };
370
+ }
371
+ if (evidence.kind === 'absent') return { evidence: null, compliance };
372
+ return { evidence: INVALID, compliance };
373
+ }
374
+
375
+ /**
376
+ * Complete a project reading with the retired policy file's presence when
377
+ * project.json did not decide the evidence (D-POLICY-JSON-RETIRED).
378
+ *
379
+ * @param {ProjectReading} reading
380
+ * @param {() => ParsedPolicy} probePolicy called only when needed
381
+ * @returns {SourceReading}
382
+ */
383
+ function withRetiredPolicy(reading, probePolicy) {
384
+ return { evidence: reading.evidence === null ? probePolicy() : reading.evidence, compliance: reading.compliance };
385
+ }
386
+
387
+ /**
388
+ * W — the working tree's project.json, then (only when it has no `evidence`)
389
+ * whether a policy.json is there. project.json is lstat-refused, never followed:
390
+ * a symlink, directory, FIFO or device is invalid unopened, and an oversize file
391
+ * invalid unread. policy.json is never opened at all.
392
+ *
393
+ * @param {string} root
394
+ * @returns {SourceReading}
395
+ */
396
+ function readWorktreeSource(root) {
397
+ const read = readBoundedRegularFile(path.join(root, '.devflow', 'project.json'), MAX_CONFIG_BYTES, false);
398
+ const project = read.kind === 'ok' ? projectConfig.parseProjectBytes(read.bytes)
399
+ : read.kind === 'absent' ? PROJECT_ABSENT : PROJECT_INVALID;
400
+ return withRetiredPolicy(projectReading(project), () => worktreePolicyPresence(root));
401
+ }
402
+
403
+ /**
404
+ * The raw `features.compliance` value from the machine manifest, or undefined.
405
+ * The manifest is read by the shared reader (lib/project-config.cjs
406
+ * readMachineManifest) — never through the CLI's manifest reader, which
407
+ * heal-writes. Anything unreadable or malformed is "no state", which
408
+ * complianceDefault maps to disabled; since C only ever raises, a missing C can
409
+ * never lower the result below the governing file.
410
+ *
411
+ * @returns {unknown}
412
+ */
413
+ function readManifestCompliance() {
414
+ const parsed = projectConfig.readMachineManifest();
415
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;
416
+ const features = /** @type {any} */ (parsed).features;
417
+ if (features === null || typeof features !== 'object' || Array.isArray(features)) return undefined;
418
+ return Object.prototype.hasOwnProperty.call(features, 'compliance') ? features.compliance : undefined;
419
+ }
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // Subprocess calls (D-POLICY-PROBE)
423
+ // ---------------------------------------------------------------------------
424
+
425
+ /**
426
+ * @typedef {{ ok: boolean, status: number | null, errorCode: string | null, stdout: Buffer, stderr: string }} CallResult
427
+ * @typedef {{ exec: ExecFn, env: NodeJS.ProcessEnv }} CallContext
428
+ */
429
+
430
+ /**
431
+ * Whether a call ran to completion and exited on its own — as opposed to never
432
+ * starting (ENOENT), timing out, overflowing its buffer or being killed. Only an
433
+ * answered non-zero exit is a real "no" ("not a repository", "no such path",
434
+ * "no such ref"); an unanswered call is NOT KNOWING, and a local-git step that
435
+ * does not know must not read as the permissive answer (avoids PF-075).
436
+ *
437
+ * @param {CallResult} r
438
+ * @returns {boolean}
439
+ */
440
+ function answered(r) {
441
+ return r.errorCode === null && r.status !== null;
442
+ }
443
+
444
+ /** @param {unknown} value @returns {Buffer} */
445
+ function asBuffer(value) {
446
+ if (Buffer.isBuffer(value)) return value;
447
+ if (typeof value === 'string') return Buffer.from(value, 'utf8');
448
+ return Buffer.alloc(0);
449
+ }
450
+
451
+ /**
452
+ * One bounded subprocess call, normalized. `ok` is exit 0 with no spawn error.
453
+ * The argv array is a fresh copy, so an exec cannot mutate a caller's constant.
454
+ *
455
+ * @param {CallContext} ctx
456
+ * @param {string} file
457
+ * @param {readonly string[]} args
458
+ * @param {string} cwd
459
+ * @param {number} timeout
460
+ * @param {number} maxBuffer
461
+ * @returns {CallResult}
462
+ */
463
+ function runCall(ctx, file, args, cwd, timeout, maxBuffer) {
464
+ const res = ctx.exec(file, [...args], {
465
+ cwd,
466
+ env: ctx.env,
467
+ stdio: ['ignore', 'pipe', 'pipe'],
468
+ timeout,
469
+ maxBuffer,
470
+ windowsHide: true,
471
+ shell: false,
472
+ });
473
+ const errorCode = res && res.error ? String(res.error.code || 'EUNKNOWN') : null;
474
+ const status = res && typeof res.status === 'number' ? res.status : null;
475
+ return {
476
+ ok: errorCode === null && status === 0,
477
+ status,
478
+ errorCode,
479
+ stdout: asBuffer(res && res.stdout),
480
+ stderr: asBuffer(res && res.stderr).toString('utf8'),
481
+ };
482
+ }
483
+
484
+ /**
485
+ * @typedef {{ kind: 'root', root: string } | { kind: 'none' } | { kind: 'unknown' }} Toplevel
486
+ */
487
+
488
+ /**
489
+ * Step 1: the repository root.
490
+ * root exit 0 with exactly one absolute path plus its newline
491
+ * none git ANSWERED non-zero — not a repository (or not a work tree); the
492
+ * resolver then treats the remote as unavailable and the worktree file
493
+ * as absent
494
+ * unknown git did not answer (missing, timed out, overflowed, killed), or
495
+ * answered exit 0 — a repository — with an unusable path. Either way a
496
+ * repository's files may exist and cannot be read, so resolve() fails
497
+ * closed rather than resolving from compliance alone
498
+ *
499
+ * @param {CallContext} ctx
500
+ * @param {string} dir
501
+ * @returns {Toplevel}
502
+ */
503
+ function gitToplevel(ctx, dir) {
504
+ const r = runCall(ctx, 'git', ['rev-parse', '--show-toplevel'], dir, GIT_LOCAL_TIMEOUT_MS, LINE_MAX_BUFFER);
505
+ if (!answered(r)) return { kind: 'unknown' };
506
+ if (r.status !== 0) return { kind: 'none' };
507
+ const text = r.stdout.toString('utf8').replace(/\r?\n$/, '');
508
+ if (text === '' || /[\r\n\0]/.test(text) || !path.isAbsolute(text)) return { kind: 'unknown' };
509
+ return { kind: 'root', root: text };
510
+ }
511
+
512
+ /**
513
+ * Step 2 — D-POLICY-PROBE: ONE gh call is both the reachability probe and the
514
+ * default-branch lookup. `{owner}/{repo}` are gh's own placeholders, filled from
515
+ * the repository's remotes (host inference is gh's). Reachable iff exit 0 with a
516
+ * SAFE_REF_RE value that is not jq's `null`; anything else — no gh, no auth, no
517
+ * GitHub remote, a hostile answer — is unavailable.
518
+ *
519
+ * @param {CallContext} ctx
520
+ * @param {string} root
521
+ * @returns {string | null} the default branch D, or null (unavailable)
522
+ */
523
+ function probeDefaultBranch(ctx, root) {
524
+ const r = runCall(ctx, 'gh', ['api', 'repos/{owner}/{repo}', '--jq', '.default_branch'], root, GH_TIMEOUT_MS, BLOB_MAX_BUFFER);
525
+ if (!r.ok) return null;
526
+ const value = r.stdout.toString('utf8').replace(/\n$/, '');
527
+ return value !== 'null' && SAFE_REF_RE.test(value) ? value : null;
528
+ }
529
+
530
+ /**
531
+ * @typedef {{ kind: 'bytes', bytes: Buffer } | { kind: 'absent' } | { kind: 'overflow' }} RemoteFile
532
+ */
533
+
534
+ /**
535
+ * Step 3: one file on the default branch — or null when the remote turned out to
536
+ * be unavailable after all.
537
+ *
538
+ * `--method GET` is mandatory: gh sends POST whenever a field is added. stdout is
539
+ * file content ONLY on exit 0 — a 404 prints gh's JSON error body on stdout, so
540
+ * it is classified from stderr and the exit code and its stdout is never parsed.
541
+ * ENOBUFS ⇒ overflow — invalid, never unavailable,
542
+ * or a huge file would hand control to
543
+ * the worktree
544
+ * exit 0 ⇒ the bytes
545
+ * exit ≠ 0 and stderr has `(HTTP 404)` ⇒ absent — the probe already succeeded,
546
+ * so 404 means "no file", not "no access"
547
+ * anything else (403/429, 5xx, timeout, spawn error) ⇒ unavailable
548
+ *
549
+ * @param {CallContext} ctx
550
+ * @param {string} root
551
+ * @param {string} ref
552
+ * @param {string} rel PROJECT_REL or POLICY_REL — a constant, never input
553
+ * @returns {RemoteFile | null}
554
+ */
555
+ function fetchRemoteFile(ctx, root, ref, rel) {
556
+ const r = runCall(ctx, 'gh', [
557
+ 'api', '--method', 'GET', 'repos/{owner}/{repo}/contents/' + rel,
558
+ '-f', 'ref=' + ref, '-H', 'Accept: application/vnd.github.raw+json',
559
+ ], root, GH_TIMEOUT_MS, BLOB_MAX_BUFFER);
560
+ if (r.errorCode === 'ENOBUFS') return { kind: 'overflow' };
561
+ if (r.ok) return { kind: 'bytes', bytes: r.stdout };
562
+ if (r.errorCode === null && r.status !== null && r.status !== 0 && r.stderr.includes('(HTTP 404)')) return { kind: 'absent' };
563
+ return null;
564
+ }
565
+
566
+ /**
567
+ * R — the default branch's project.json, then (only when it is a 404 or has no
568
+ * `evidence`) whether its policy.json exists — the contents call's bytes are
569
+ * discarded unread (D-POLICY-JSON-RETIRED). Null when either call finds the
570
+ * remote unavailable: a half-read default branch is not a default branch.
571
+ *
572
+ * @param {CallContext} ctx
573
+ * @param {string} root
574
+ * @param {string} ref
575
+ * @returns {SourceReading | null}
576
+ */
577
+ function remoteSource(ctx, root, ref) {
578
+ const projectFile = fetchRemoteFile(ctx, root, ref, PROJECT_REL);
579
+ if (projectFile === null) return null;
580
+ const reading = projectReading(projectFile.kind === 'bytes' ? projectConfig.parseProjectBytes(projectFile.bytes)
581
+ : projectFile.kind === 'absent' ? PROJECT_ABSENT : PROJECT_INVALID);
582
+ if (reading.evidence !== null) return { evidence: reading.evidence, compliance: reading.compliance };
583
+ const policyFile = fetchRemoteFile(ctx, root, ref, POLICY_REL);
584
+ if (policyFile === null) return null;
585
+ return withRetiredPolicy(reading, () => retiredPolicyReading(policyFile.kind !== 'absent'));
586
+ }
587
+
588
+ /**
589
+ * Step 5 (offline, D unknown): the default branch from `ls-remote --symref`.
590
+ * Only line 1 is read, and its capture must still pass SAFE_REF_RE.
591
+ *
592
+ * @param {CallContext} ctx
593
+ * @param {string} root
594
+ * @returns {string | null}
595
+ */
596
+ function lsRemoteDefaultBranch(ctx, root) {
597
+ const r = runCall(ctx, 'git', ['ls-remote', '--symref', 'origin', 'HEAD'], root, GIT_REMOTE_TIMEOUT_MS, LINE_MAX_BUFFER);
598
+ if (!r.ok) return null;
599
+ const text = r.stdout.toString('utf8');
600
+ const newline = text.indexOf('\n');
601
+ const m = LS_REMOTE_SYMREF_RE.exec(newline === -1 ? text : text.slice(0, newline));
602
+ return m !== null && SAFE_REF_RE.test(m[1]) ? m[1] : null;
603
+ }
604
+
605
+ /**
606
+ * @typedef {{ kind: 'ref', ref: string } | { kind: 'none' } | { kind: 'unreadable' }} OriginHead
607
+ */
608
+
609
+ /**
610
+ * Step 5b (offline, D still unknown after ls-remote) — D-OFFLINE-ORIGIN-HEAD:
611
+ * the default branch this clone last recorded for origin, read LOCALLY with
612
+ * `git symbolic-ref --quiet refs/remotes/origin/HEAD` (no network, no index
613
+ * refresh). With D unnamed an unreachable origin leaves the worktree's own file
614
+ * governing, so a branch that sets `evidence:"standard"` would resolve standard
615
+ * while main says required; naming D here lets trackingSource fold main's
616
+ * tracking copy in, so the branch cannot lower it.
617
+ * ref exit 0 with one `refs/remotes/origin/<SAFE_REF_RE>` line
618
+ * none an answered non-zero exit: origin/HEAD is not recorded — the
619
+ * residual case, where the worktree file governs with
620
+ * WARN=remote-unavailable
621
+ * unreadable git did not answer, or origin/HEAD exists but its answer names
622
+ * no safe branch (another remote, a hostile or unparseable name).
623
+ * Either is a failure, never the residual case: gatherFacts reads
624
+ * it as an invalid base, which resolves required (avoids PF-075).
625
+ * resolve-settings' defaultBranchCompliance fails the same answer
626
+ * closed, to `generic`.
627
+ *
628
+ * @param {CallContext} ctx
629
+ * @param {string} root
630
+ * @returns {OriginHead}
631
+ */
632
+ function localOriginHead(ctx, root) {
633
+ const r = runCall(ctx, 'git', ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], root,
634
+ GIT_LOCAL_TIMEOUT_MS, LINE_MAX_BUFFER);
635
+ if (!answered(r)) return { kind: 'unreadable' };
636
+ if (!r.ok) return { kind: 'none' };
637
+ const ref = projectConfig.parseOriginHeadRef(r.stdout.toString('utf8'));
638
+ return ref === null ? { kind: 'unreadable' } : { kind: 'ref', ref };
639
+ }
640
+
641
+ /**
642
+ * Steps 4 and 7: one blob at a revision (HEAD, or the tracking ref).
643
+ * Exit 0 ⇒ the bytes; an answered non-zero exit (the path does not exist there,
644
+ * an unborn HEAD) ⇒ absent; an unanswered call (ENOBUFS included) ⇒ unanswered,
645
+ * which every caller reads as invalid — never absent, or a git that timed out
646
+ * would silently drop the default branch's copy from the fold.
647
+ *
648
+ * @param {CallContext} ctx
649
+ * @param {string} root
650
+ * @param {string} revision
651
+ * @param {string} rel PROJECT_REL or POLICY_REL — a constant, never input
652
+ * @returns {{ kind: 'bytes', bytes: Buffer } | { kind: 'absent' } | { kind: 'unanswered' }}
653
+ */
654
+ function catFileBlob(ctx, root, revision, rel) {
655
+ const r = runCall(ctx, 'git', ['cat-file', 'blob', revision + ':' + rel], root,
656
+ GIT_LOCAL_TIMEOUT_MS, BLOB_MAX_BUFFER);
657
+ if (r.ok) return { kind: 'bytes', bytes: r.stdout };
658
+ return answered(r) ? { kind: 'absent' } : { kind: 'unanswered' };
659
+ }
660
+
661
+ /**
662
+ * Whether a revision holds the retired policy blob (D-POLICY-JSON-RETIRED): its
663
+ * bytes are discarded unread, and an unanswered read cannot prove absence, so it
664
+ * reads as present.
665
+ *
666
+ * @param {CallContext} ctx
667
+ * @param {string} root
668
+ * @param {string} revision
669
+ * @returns {ParsedPolicy}
670
+ */
671
+ function catFilePolicyPresence(ctx, root, revision) {
672
+ return retiredPolicyReading(catFileBlob(ctx, root, revision, POLICY_REL).kind !== 'absent');
673
+ }
674
+
675
+ /**
676
+ * T or H: a revision's project.json, then (only when it is absent or has no
677
+ * `evidence`) whether its policy.json exists.
678
+ *
679
+ * @param {CallContext} ctx
680
+ * @param {string} root
681
+ * @param {string} revision
682
+ * @returns {SourceReading}
683
+ */
684
+ function revisionSource(ctx, root, revision) {
685
+ const blob = catFileBlob(ctx, root, revision, PROJECT_REL);
686
+ const project = blob.kind === 'bytes' ? projectConfig.parseProjectBytes(blob.bytes)
687
+ : blob.kind === 'absent' ? PROJECT_ABSENT : PROJECT_INVALID;
688
+ return withRetiredPolicy(projectReading(project), () => catFilePolicyPresence(ctx, root, revision));
689
+ }
690
+
691
+ /**
692
+ * Steps 6 and 7 (offline, D known): T, the local tracking copy of the default
693
+ * branch's files. The ref check decides whether T can stand for the default branch
694
+ * at all — a ref git ANSWERS is missing is "B unknown" (null), which a failed blob
695
+ * read could not tell apart from "file absent". A ref check git does not answer is
696
+ * not knowing, so T is invalid (it raises) rather than unknown (it would not).
697
+ *
698
+ * @param {CallContext} ctx
699
+ * @param {string} root
700
+ * @param {string} ref
701
+ * @returns {SourceReading | null}
702
+ */
703
+ function trackingSource(ctx, root, ref) {
704
+ const trackingRef = 'refs/remotes/origin/' + ref;
705
+ const r = runCall(ctx, 'git', ['rev-parse', '--verify', '--quiet', trackingRef], root,
706
+ GIT_LOCAL_TIMEOUT_MS, LINE_MAX_BUFFER);
707
+ if (r.ok) return revisionSource(ctx, root, trackingRef);
708
+ return answered(r) ? null : { evidence: INVALID, compliance: false };
709
+ }
710
+
711
+ // ---------------------------------------------------------------------------
712
+ // Gathering the facts (the imperative shell)
713
+ // ---------------------------------------------------------------------------
714
+
715
+ /**
716
+ * @typedef {{
717
+ * reachable: boolean,
718
+ * ref: string | null,
719
+ * remote: ParsedPolicy | null,
720
+ * worktree: ParsedPolicy,
721
+ * tracking: ParsedPolicy | null,
722
+ * head: ParsedPolicy | null,
723
+ * compliance: Policy,
724
+ * }} Facts
725
+ * remote R — set iff reachable
726
+ * tracking T — set iff offline and refs/remotes/origin/<D> exists (or git
727
+ * could not answer whether it does, or origin/HEAD is unreadable —
728
+ * then invalid)
729
+ * head H — set iff B (R online, T offline) is known
730
+ * compliance stricter(C_machine, C_repo) — D-COMPLIANCE-REPO-FLOOR
731
+ */
732
+
733
+ /**
734
+ * Run the fixed call sequence (D-POLICY-PROBE). The only calls ever made, in order:
735
+ * git rev-parse --show-toplevel (cwd = <dir>)
736
+ * gh api repos/{owner}/{repo} --jq .default_branch
737
+ * gh api --method GET …/contents/.devflow/project.json (the probe named D)
738
+ * gh api --method GET …/contents/.devflow/policy.json (… and project.json is
739
+ * a 404 or has no evidence;
740
+ * presence only)
741
+ * git ls-remote --symref origin HEAD (offline, D unknown)
742
+ * git symbolic-ref --quiet refs/remotes/origin/HEAD (… and ls-remote named none)
743
+ * git rev-parse --verify --quiet refs/remotes/origin/D (offline, D known)
744
+ * git cat-file blob refs/remotes/origin/D:…/project.json (that ref exists)
745
+ * git cat-file blob refs/remotes/origin/D:…/policy.json (… no evidence there; presence)
746
+ * git cat-file blob HEAD:…/project.json (B known)
747
+ * git cat-file blob HEAD:…/policy.json (… no evidence there; presence)
748
+ *
749
+ * Returns null when git cannot say whether <dir> is in a repository at all (see
750
+ * gitToplevel): nothing below can be trusted then, and resolve() fails closed.
751
+ *
752
+ * @param {string} dir
753
+ * @param {Policy} compliance C_machine
754
+ * @param {ExecFn} exec
755
+ * @returns {Facts | null}
756
+ */
757
+ function gatherFacts(dir, compliance, exec) {
758
+ /** @type {CallContext} */
759
+ const ctx = {
760
+ exec,
761
+ env: Object.assign({}, process.env, { GH_PROMPT_DISABLED: '1', GIT_TERMINAL_PROMPT: '0' }),
762
+ };
763
+
764
+ const toplevel = gitToplevel(ctx, dir);
765
+ if (toplevel.kind === 'unknown') return null;
766
+ if (toplevel.kind === 'none') {
767
+ return { reachable: false, ref: null, remote: null, worktree: ABSENT, tracking: null, head: null, compliance };
768
+ }
769
+ const root = toplevel.root;
770
+
771
+ const worktree = readWorktreeSource(root);
772
+ let ref = probeDefaultBranch(ctx, root);
773
+ const remote = ref === null ? null : remoteSource(ctx, root, ref);
774
+ const reachable = remote !== null;
775
+
776
+ /** @type {SourceReading | null} */
777
+ let tracking = null;
778
+ if (!reachable) {
779
+ if (ref === null) ref = lsRemoteDefaultBranch(ctx, root);
780
+ if (ref === null) {
781
+ const originHead = localOriginHead(ctx, root);
782
+ if (originHead.kind === 'ref') ref = originHead.ref;
783
+ if (originHead.kind === 'unreadable') tracking = { evidence: INVALID, compliance: false };
784
+ }
785
+ if (ref !== null) tracking = trackingSource(ctx, root, ref);
786
+ }
787
+
788
+ const baseKnown = reachable || tracking !== null;
789
+ const head = baseKnown ? revisionSource(ctx, root, 'HEAD') : null;
790
+ const repoCompliance = worktree.compliance
791
+ || (remote !== null && remote.compliance)
792
+ || (tracking !== null && tracking.compliance);
793
+ return {
794
+ reachable,
795
+ ref,
796
+ remote: remote === null ? null : remote.evidence,
797
+ worktree: worktree.evidence,
798
+ tracking: tracking === null ? null : tracking.evidence,
799
+ head: head === null ? null : head.evidence,
800
+ compliance: stricter(compliance, repoCompliance ? 'required' : 'standard'),
801
+ };
802
+ }
803
+
804
+ // ---------------------------------------------------------------------------
805
+ // The fold (the functional core)
806
+ // ---------------------------------------------------------------------------
807
+
808
+ /**
809
+ * @param {Policy} a
810
+ * @param {Policy} b
811
+ * @returns {Policy}
812
+ */
813
+ function stricter(a, b) {
814
+ return a === 'required' || b === 'required' ? 'required' : 'standard';
815
+ }
816
+
817
+ /**
818
+ * The comparable state of a parsed file: absent, invalid, or its policy.
819
+ *
820
+ * @param {ParsedPolicy} p
821
+ * @returns {string}
822
+ */
823
+ function stateOf(p) {
824
+ return p.kind === 'valid' ? p.policy : p.kind;
825
+ }
826
+
827
+ /**
828
+ * D-POLICY-FOLD: the stricter value always wins, and local sources only raise.
829
+ *
830
+ * The governing file decides SOURCE and the base — R when the remote is
831
+ * reachable, W when it is not:
832
+ * valid v ⇒ file|worktree, base v invalid ⇒ invalid, base required
833
+ * absent ⇒ default, base C
834
+ * The classifier is total by construction: the terminal arm is the conservative
835
+ * one (invalid, required), so an unforeseen state can only tighten.
836
+ *
837
+ * Then, raise-only: online W folds in (a valid v contributes v, invalid
838
+ * contributes required); offline T folds in when pr-changes-policy fires; C
839
+ * always folds in. `raised-by-compliance` fires only for SOURCE file|worktree —
840
+ * under `default`, C IS the default rather than a raise.
841
+ *
842
+ * D-POLICY-CHANGE-DETECT: `pr-changes-policy` fires iff B (the default branch's
843
+ * state — R online, T offline) is known and state(H) ≠ state(B) or state(W) ≠
844
+ * state(B). The comparison is SEMANTIC (absent/invalid/required/standard), so a
845
+ * CRLF checkout or reformatted JSON does not fire it. It means "the branch or
846
+ * working tree differs from the default branch": a branch cut before a later
847
+ * default-branch edit fires it too, and rebasing clears it — consumers must
848
+ * treat it as advisory. It never lowers: online B is already the base and W can
849
+ * only raise; offline T is folded in, so a branch that edits or deletes the file
850
+ * cannot drop below the default-branch copy.
851
+ *
852
+ * @param {Facts} facts
853
+ * @returns {Resolution}
854
+ */
855
+ function foldPolicy(facts) {
856
+ /** @type {Set<Warning>} */
857
+ const warnings = new Set();
858
+ if (!facts.reachable) warnings.add('remote-unavailable');
859
+
860
+ const governing = facts.reachable && facts.remote !== null ? facts.remote : facts.worktree;
861
+ /** @type {Source} */
862
+ let source;
863
+ /** @type {Policy} */
864
+ let policy;
865
+ if (governing.kind === 'valid') {
866
+ source = facts.reachable ? 'file' : 'worktree';
867
+ policy = governing.policy;
868
+ } else if (governing.kind === 'absent') {
869
+ source = 'default';
870
+ policy = facts.compliance;
871
+ } else {
872
+ source = 'invalid';
873
+ policy = 'required';
874
+ warnings.add('invalid-file');
875
+ }
876
+
877
+ const base = facts.reachable ? facts.remote : facts.tracking;
878
+ if (base !== null) {
879
+ const baseState = stateOf(base);
880
+ const headDiffers = facts.head !== null && stateOf(facts.head) !== baseState;
881
+ if (headDiffers || stateOf(facts.worktree) !== baseState) warnings.add('pr-changes-policy');
882
+ }
883
+
884
+ /** @type {ParsedPolicy[]} */
885
+ const folded = [];
886
+ if (facts.reachable) folded.push(facts.worktree);
887
+ if (!facts.reachable && facts.tracking !== null && warnings.has('pr-changes-policy')) folded.push(facts.tracking);
888
+ for (const file of folded) {
889
+ if (file.kind === 'valid') {
890
+ policy = stricter(policy, file.policy);
891
+ } else if (file.kind === 'invalid') {
892
+ policy = 'required';
893
+ warnings.add('invalid-file');
894
+ }
895
+ }
896
+
897
+ const withoutCompliance = policy;
898
+ policy = stricter(policy, facts.compliance);
899
+ if (facts.compliance === 'required' && withoutCompliance === 'standard'
900
+ && (source === 'file' || source === 'worktree')) {
901
+ warnings.add('raised-by-compliance');
902
+ }
903
+
904
+ return Object.freeze({
905
+ policy,
906
+ source,
907
+ ref: facts.ref === null ? 'none' : facts.ref,
908
+ warnings: Object.freeze(WARNINGS.filter(w => warnings.has(w))),
909
+ inputs: MECHANISM_INPUTS[policy],
910
+ });
911
+ }
912
+
913
+ // ---------------------------------------------------------------------------
914
+ // resolve
915
+ // ---------------------------------------------------------------------------
916
+
917
+ /**
918
+ * The production exec: spawnSync, read off the module object at call time.
919
+ *
920
+ * @type {ExecFn}
921
+ */
922
+ function defaultExec(file, args, opts) {
923
+ return childProcess.spawnSync(file, args, /** @type {any} */ (opts));
924
+ }
925
+
926
+ /**
927
+ * Resolve the policy for `opts.dir`. Never throws: an unexpected internal
928
+ * failure (the exec itself throwing, say), or a git that cannot say whether
929
+ * `opts.dir` is in a repository, is the fail-closed resolution — `required`,
930
+ * SOURCE `error`, REF `none` — which main() maps to exit 4.
931
+ *
932
+ * @param {ResolveOptions} opts
933
+ * @param {ResolveDeps} [deps]
934
+ * @returns {Resolution}
935
+ */
936
+ function resolve(opts, deps) {
937
+ try {
938
+ const exec = deps && typeof deps.exec === 'function' ? deps.exec : defaultExec;
939
+ const compliance = complianceDefault(opts.compliance !== undefined ? opts.compliance : readManifestCompliance());
940
+ const facts = gatherFacts(opts.dir, compliance, exec);
941
+ return facts === null ? ERROR_RESOLUTION : foldPolicy(facts);
942
+ } catch (_) {
943
+ return ERROR_RESOLUTION;
944
+ }
945
+ }
946
+
947
+ // ---------------------------------------------------------------------------
948
+ // The line, and the gate on it
949
+ // ---------------------------------------------------------------------------
950
+
951
+ /**
952
+ * Compose the stdout line for a resolution (D-POLICY-LINE). WARN is omitted when
953
+ * empty; the mechanism inputs follow in their fixed order.
954
+ *
955
+ * @param {Resolution} r
956
+ * @returns {string}
957
+ */
958
+ function formatLine(r) {
959
+ const warn = r.warnings.length > 0 ? ' WARN=' + r.warnings.join(',') : '';
960
+ const inputs = INPUT_KEYS.map(k => k + '=' + String(r.inputs[/** @type {keyof MechanismInputs} */ (k)])).join(' ');
961
+ return 'EVIDENCE_POLICY=' + r.policy + ' SOURCE=' + r.source + ' REF=' + r.ref + warn + ' ' + inputs;
962
+ }
963
+
964
+ /**
965
+ * Whether a line is well-formed AND internally coherent: it matches
966
+ * OUTPUT_LINE_RE, its inputs are exactly MECHANISM_INPUTS for its policy, its
967
+ * WARN tokens are unique and in registry order, and SOURCE invalid|error carries
968
+ * `required`.
969
+ *
970
+ * @param {unknown} line
971
+ * @returns {boolean}
972
+ */
973
+ function isCoherentLine(line) {
974
+ if (typeof line !== 'string') return false;
975
+ const m = OUTPUT_LINE_RE.exec(line);
976
+ if (m === null || m.groups === undefined) return false;
977
+ const g = m.groups;
978
+ const expected = MECHANISM_INPUTS[/** @type {Policy} */ (g.policy)];
979
+ if (g.issue !== String(expected.ISSUE_REQUIRED)
980
+ || g.conventions !== String(expected.APPLY_CONVENTIONS)
981
+ || g.approval !== String(expected.REQUIRE_NON_AUTHOR_APPROVAL)) {
982
+ return false;
983
+ }
984
+ if ((g.source === 'invalid' || g.source === 'error') && g.policy !== 'required') return false;
985
+ if (g.warn !== undefined) {
986
+ const order = g.warn.split(',').map(t => WARNINGS.indexOf(/** @type {Warning} */ (t)));
987
+ for (let i = 1; i < order.length; i++) {
988
+ if (order[i] <= order[i - 1]) return false;
989
+ }
990
+ }
991
+ return true;
992
+ }
993
+
994
+ /**
995
+ * THE GATE main() applies before returning a line: coherent, and carrying
996
+ * exactly the resolved policy — a line that would LOWER the policy is refused
997
+ * even when it is otherwise perfect.
998
+ *
999
+ * @param {unknown} line
1000
+ * @param {Resolution} resolution
1001
+ * @returns {boolean}
1002
+ */
1003
+ function passesOutputGate(line, resolution) {
1004
+ if (!isCoherentLine(line)) return false;
1005
+ const m = OUTPUT_LINE_RE.exec(/** @type {string} */ (line));
1006
+ return m !== null && m.groups !== undefined && m.groups.policy === resolution.policy;
1007
+ }
1008
+
1009
+ /**
1010
+ * Settle whatever main() returned into what the boundary may print. The code
1011
+ * must be a known exit code and the line must fit it: '' for usage, a coherent
1012
+ * line for 0, FAIL_CLOSED_LINE for the refusals. Anything else is refused.
1013
+ *
1014
+ * @param {unknown} outcome
1015
+ * @returns {MainOutcome}
1016
+ */
1017
+ function settleOutcome(outcome) {
1018
+ const o = /** @type {any} */ (outcome);
1019
+ if (o === null || typeof o !== 'object' || typeof o.line !== 'string') {
1020
+ return { code: EXIT_CODES.INTERNAL_ERROR, line: FAIL_CLOSED_LINE };
1021
+ }
1022
+ if (o.code === EXIT_CODES.USAGE) return { code: EXIT_CODES.USAGE, line: '' };
1023
+ if (o.code === EXIT_CODES.RESOLVED) {
1024
+ return isCoherentLine(o.line) ? { code: o.code, line: o.line } : { code: EXIT_CODES.OUTPUT_GATE_REFUSED, line: FAIL_CLOSED_LINE };
1025
+ }
1026
+ if (o.code === EXIT_CODES.INPUT_UNUSABLE || o.code === EXIT_CODES.OUTPUT_GATE_REFUSED) {
1027
+ return { code: o.code, line: FAIL_CLOSED_LINE };
1028
+ }
1029
+ return { code: EXIT_CODES.INTERNAL_ERROR, line: FAIL_CLOSED_LINE };
1030
+ }
1031
+
1032
+ /**
1033
+ * A short, safe label for a thrown value — its class name and system error code,
1034
+ * never its message (a message can quote input bytes).
1035
+ *
1036
+ * @param {unknown} err
1037
+ * @returns {string}
1038
+ */
1039
+ function errorLabel(err) {
1040
+ const e = /** @type {any} */ (err);
1041
+ const name = e && typeof e.name === 'string' ? e.name : 'unknown';
1042
+ const code = e && typeof e.code === 'string' ? '/' + e.code : '';
1043
+ return (name + code).replace(/[^A-Za-z0-9_/]/g, '').slice(0, 60);
1044
+ }
1045
+
1046
+ // ---------------------------------------------------------------------------
1047
+ // main — returns {code, line}; never calls process.exit
1048
+ // ---------------------------------------------------------------------------
1049
+
1050
+ /**
1051
+ * @param {string} dir
1052
+ * @returns {boolean}
1053
+ */
1054
+ function isUsableDirectory(dir) {
1055
+ try {
1056
+ return fs.statSync(dir).isDirectory();
1057
+ } catch (_) {
1058
+ return false;
1059
+ }
1060
+ }
1061
+
1062
+ /**
1063
+ * @param {readonly string[]} argv process.argv
1064
+ * @param {MainDeps} [deps]
1065
+ * @returns {MainOutcome}
1066
+ */
1067
+ function main(argv, deps) {
1068
+ const d = deps || {};
1069
+ const args = parseArgs(argv);
1070
+ if (args.kind === 'usage') {
1071
+ process.stderr.write(args.usage + '\n');
1072
+ return { code: EXIT_CODES.USAGE, line: '' };
1073
+ }
1074
+
1075
+ if (!isUsableDirectory(args.dir)) {
1076
+ process.stderr.write('resolve-evidence-policy: not a usable directory: '
1077
+ + JSON.stringify(args.dir).slice(0, 300) + ' — failing closed to required\n');
1078
+ return { code: EXIT_CODES.INPUT_UNUSABLE, line: FAIL_CLOSED_LINE };
1079
+ }
1080
+
1081
+ const resolution = resolve({ dir: args.dir }, { exec: d.exec });
1082
+ if (resolution.source === 'error') {
1083
+ process.stderr.write('resolve-evidence-policy: could not resolve (git did not answer, or an internal error)'
1084
+ + ' — failing closed to required\n');
1085
+ return { code: EXIT_CODES.INTERNAL_ERROR, line: FAIL_CLOSED_LINE };
1086
+ }
1087
+
1088
+ const format = typeof d.formatLine === 'function' ? d.formatLine : formatLine;
1089
+ let line;
1090
+ try {
1091
+ line = format(resolution);
1092
+ } catch (err) {
1093
+ process.stderr.write('resolve-evidence-policy: internal error (' + errorLabel(err) + ') — failing closed to required\n');
1094
+ return { code: EXIT_CODES.INTERNAL_ERROR, line: FAIL_CLOSED_LINE };
1095
+ }
1096
+
1097
+ if (!passesOutputGate(line, resolution)) {
1098
+ process.stderr.write('resolve-evidence-policy: output gate refused the composed line — failing closed to required\n');
1099
+ return { code: EXIT_CODES.OUTPUT_GATE_REFUSED, line: FAIL_CLOSED_LINE };
1100
+ }
1101
+ return { code: EXIT_CODES.RESOLVED, line: /** @type {string} */ (line) };
1102
+ }
1103
+
1104
+ // ---------------------------------------------------------------------------
1105
+ // Top-level boundary
1106
+ //
1107
+ // This is the ONLY place that writes to stdout and sets process.exitCode.
1108
+ // It re-checks what main() returned (settleOutcome) before writing, so even a
1109
+ // main() that returned something malformed prints either nothing (usage) or a
1110
+ // line that passes the gate. Its catch is the fail-closed arm for anything
1111
+ // main() did not anticipate.
1112
+ // ---------------------------------------------------------------------------
1113
+
1114
+ if (require.main === module) {
1115
+ let outcome;
1116
+ try {
1117
+ outcome = main(process.argv);
1118
+ } catch (err) {
1119
+ process.stderr.write('resolve-evidence-policy: internal error (' + errorLabel(err) + ') — failing closed to required\n');
1120
+ outcome = { code: EXIT_CODES.INTERNAL_ERROR, line: FAIL_CLOSED_LINE };
1121
+ }
1122
+ const settled = settleOutcome(outcome);
1123
+ if (settled.line !== '') process.stdout.write(settled.line + '\n');
1124
+ process.exitCode = settled.code;
1125
+ }
1126
+
1127
+ // ---------------------------------------------------------------------------
1128
+ // Exports — the CLI's `compliance --status` seam and the unit tests
1129
+ // ---------------------------------------------------------------------------
1130
+
1131
+ module.exports = Object.freeze({
1132
+ POLICIES,
1133
+ SOURCES,
1134
+ WARNINGS,
1135
+ EXIT_CODES,
1136
+ MECHANISM_INPUTS,
1137
+ SAFE_REF_RE,
1138
+ OUTPUT_LINE_RE,
1139
+ FAIL_CLOSED_LINE,
1140
+ parseArgs,
1141
+ complianceDefault,
1142
+ resolve,
1143
+ formatLine,
1144
+ main,
1145
+ });