@lannguyensi/harness 0.56.0 → 0.58.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 (126) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/README.md +1 -1
  3. package/dist/cli/apply/apply.js +4 -3
  4. package/dist/cli/apply/apply.js.map +1 -1
  5. package/dist/cli/apply/generate-codex-config.js +3 -3
  6. package/dist/cli/apply/generate-codex-config.js.map +1 -1
  7. package/dist/cli/apply/generate-memory-index.js +20 -10
  8. package/dist/cli/apply/generate-memory-index.js.map +1 -1
  9. package/dist/cli/apply/generate-settings.d.ts +7 -0
  10. package/dist/cli/apply/generate-settings.js +10 -1
  11. package/dist/cli/apply/generate-settings.js.map +1 -1
  12. package/dist/cli/approve/understanding.d.ts +4 -4
  13. package/dist/cli/doctor/format.js +68 -2
  14. package/dist/cli/doctor/format.js.map +1 -1
  15. package/dist/cli/doctor/index.d.ts +6 -0
  16. package/dist/cli/doctor/index.js +214 -28
  17. package/dist/cli/doctor/index.js.map +1 -1
  18. package/dist/cli/doctor/session-start-preflight-setup-version.d.ts +83 -0
  19. package/dist/cli/doctor/session-start-preflight-setup-version.js +114 -0
  20. package/dist/cli/doctor/session-start-preflight-setup-version.js.map +1 -0
  21. package/dist/cli/doctor/types.d.ts +35 -4
  22. package/dist/cli/doctor/types.js +2 -1
  23. package/dist/cli/doctor/types.js.map +1 -1
  24. package/dist/cli/doctor/ug-delegations.js +9 -0
  25. package/dist/cli/doctor/ug-delegations.js.map +1 -1
  26. package/dist/cli/explain-policy.d.ts +67 -0
  27. package/dist/cli/explain-policy.js +140 -3
  28. package/dist/cli/explain-policy.js.map +1 -1
  29. package/dist/cli/gc/index.js +28 -14
  30. package/dist/cli/gc/index.js.map +1 -1
  31. package/dist/cli/index.d.ts +1 -0
  32. package/dist/cli/index.js +10 -7
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/init/composer.js +4 -3
  35. package/dist/cli/init/composer.js.map +1 -1
  36. package/dist/cli/init/dependencies.js +15 -9
  37. package/dist/cli/init/dependencies.js.map +1 -1
  38. package/dist/cli/init/templates.d.ts +1 -0
  39. package/dist/cli/init/templates.js +30 -4
  40. package/dist/cli/init/templates.js.map +1 -1
  41. package/dist/cli/list.js +19 -0
  42. package/dist/cli/list.js.map +1 -1
  43. package/dist/cli/loader.js +16 -1
  44. package/dist/cli/loader.js.map +1 -1
  45. package/dist/cli/pack/hook-bootstrap.js +2 -1
  46. package/dist/cli/pack/hook-bootstrap.js.map +1 -1
  47. package/dist/cli/pack/hook-codex-user-prompt-submit.d.ts +0 -32
  48. package/dist/cli/pack/hook-codex-user-prompt-submit.js +11 -118
  49. package/dist/cli/pack/hook-codex-user-prompt-submit.js.map +1 -1
  50. package/dist/cli/pack/hook-pre-tool-use.js +34 -7
  51. package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
  52. package/dist/cli/pack/hook-solution-acceptance.d.ts +55 -0
  53. package/dist/cli/pack/hook-solution-acceptance.js +222 -16
  54. package/dist/cli/pack/hook-solution-acceptance.js.map +1 -1
  55. package/dist/cli/pack/hook-stay-in-scope.d.ts +5 -26
  56. package/dist/cli/pack/hook-stay-in-scope.js +62 -274
  57. package/dist/cli/pack/hook-stay-in-scope.js.map +1 -1
  58. package/dist/cli/pack/hook-track-active-claim.d.ts +1 -0
  59. package/dist/cli/pack/hook-track-active-claim.js +107 -43
  60. package/dist/cli/pack/hook-track-active-claim.js.map +1 -1
  61. package/dist/cli/session-start/index.d.ts +32 -1
  62. package/dist/cli/session-start/index.js +142 -12
  63. package/dist/cli/session-start/index.js.map +1 -1
  64. package/dist/cli/validate/checks.d.ts +0 -2
  65. package/dist/cli/validate/checks.js +30 -30
  66. package/dist/cli/validate/checks.js.map +1 -1
  67. package/dist/io/harness-lock.js +11 -1
  68. package/dist/io/harness-lock.js.map +1 -1
  69. package/dist/io/lock.d.ts +26 -0
  70. package/dist/io/lock.js +25 -0
  71. package/dist/io/lock.js.map +1 -1
  72. package/dist/io/project-name.d.ts +115 -0
  73. package/dist/io/project-name.js +158 -0
  74. package/dist/io/project-name.js.map +1 -0
  75. package/dist/io/read-json-dir-entries.js +10 -0
  76. package/dist/io/read-json-dir-entries.js.map +1 -1
  77. package/dist/io/version-compare.d.ts +59 -0
  78. package/dist/io/version-compare.js +67 -0
  79. package/dist/io/version-compare.js.map +1 -1
  80. package/dist/policy-packs/builtin/solution-acceptance-reconnect.d.ts +50 -0
  81. package/dist/policy-packs/builtin/solution-acceptance-reconnect.js +151 -0
  82. package/dist/policy-packs/builtin/solution-acceptance-reconnect.js.map +1 -0
  83. package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +43 -4
  84. package/dist/policy-packs/builtin/solution-acceptance-runtime.js +43 -39
  85. package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
  86. package/dist/policy-packs/builtin/solution-acceptance.js +4 -2
  87. package/dist/policy-packs/builtin/solution-acceptance.js.map +1 -1
  88. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js +15 -0
  89. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js.map +1 -1
  90. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.js +5 -7
  91. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.js.map +1 -1
  92. package/dist/policy-packs/builtin/understanding-before-execution/markers.js +23 -0
  93. package/dist/policy-packs/builtin/understanding-before-execution/markers.js.map +1 -1
  94. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js +8 -17
  95. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js.map +1 -1
  96. package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.d.ts +129 -0
  97. package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.js +166 -0
  98. package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.js.map +1 -0
  99. package/dist/policy-packs/builtin/understanding-before-execution.d.ts +129 -0
  100. package/dist/policy-packs/builtin/understanding-before-execution.js +44 -57
  101. package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
  102. package/dist/policy-packs/version-check.d.ts +8 -1
  103. package/dist/policy-packs/version-check.js +10 -6
  104. package/dist/policy-packs/version-check.js.map +1 -1
  105. package/dist/probes/memory.d.ts +34 -0
  106. package/dist/probes/memory.js +87 -14
  107. package/dist/probes/memory.js.map +1 -1
  108. package/dist/runtime/git-context.d.ts +125 -0
  109. package/dist/runtime/git-context.js +164 -1
  110. package/dist/runtime/git-context.js.map +1 -1
  111. package/dist/runtime/index.d.ts +1 -1
  112. package/dist/runtime/index.js +1 -1
  113. package/dist/runtime/index.js.map +1 -1
  114. package/dist/runtime/task-providers/agent-tasks.d.ts +90 -0
  115. package/dist/runtime/task-providers/agent-tasks.js +231 -0
  116. package/dist/runtime/task-providers/agent-tasks.js.map +1 -0
  117. package/dist/runtime/workflow-policies.d.ts +1 -0
  118. package/dist/runtime/workflow-policies.js +8 -8
  119. package/dist/runtime/workflow-policies.js.map +1 -1
  120. package/dist/schema/index.d.ts +19 -0
  121. package/dist/schema/index.js +5 -0
  122. package/dist/schema/index.js.map +1 -1
  123. package/dist/schema/session-start-preflight.d.ts +10 -0
  124. package/dist/schema/session-start-preflight.js +106 -0
  125. package/dist/schema/session-start-preflight.js.map +1 -0
  126. package/package.json +7 -2
@@ -0,0 +1,158 @@
1
+ // `{project}` value validation and safe display, shared by every
2
+ // `{project}`-shaped interpolation sink.
3
+ //
4
+ // Lives in `src/io/` rather than `src/runtime/` (where `isValidProjectName`
5
+ // and `sanitizeProjectForDisplay` originated, task `c88461c1`) because
6
+ // `buildLockEntries` (`src/io/harness-lock.ts`, task `b5e6ccb0`) is itself
7
+ // one of those sinks, and `io/` may not import from `runtime/`
8
+ // (`io-no-upward-imports`, `.dependency-cruiser.cjs`). This is the same
9
+ // relocation pattern `.dependency-cruiser.cjs`'s header comment already
10
+ // documents for `src/policies/duration.ts`, `src/policies/extract.ts`,
11
+ // `src/runtime/ledger-record.ts` and `src/runtime/expand-home.ts`: a
12
+ // utility needed below its original layer moves down rather than the
13
+ // importing layer's boundary being relaxed. `src/runtime/git-context.ts`
14
+ // re-exports both names so every existing consumer that imports them from
15
+ // `../runtime/git-context.js` keeps working unchanged.
16
+ /**
17
+ * Every C0 control character (U+0000 to U+001F, so NUL, LF, CR and ESC
18
+ * among them), DEL (U+007F), and every C1 control character (U+0080 to
19
+ * U+009F, NEL included). Shared by {@link isValidProjectName}, which
20
+ * rejects a name carrying one, and {@link sanitizeProjectForDisplay},
21
+ * which strips them from a name that was already rejected and is being
22
+ * echoed back.
23
+ */
24
+ const PROJECT_NAME_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/;
25
+ /**
26
+ * {@link PROJECT_NAME_CONTROL_CHARS} with the `g` flag, for
27
+ * `String.prototype.replace`. Two objects built from one source rather
28
+ * than one shared `g`-flagged regex, because a `g`-flagged regex carries
29
+ * a stateful `lastIndex` that makes `.test()` alternate between hits and
30
+ * misses across calls.
31
+ */
32
+ const PROJECT_NAME_CONTROL_CHARS_GLOBAL = new RegExp(PROJECT_NAME_CONTROL_CHARS.source, "g");
33
+ /**
34
+ * Is `name` usable as a `{project}` value at all: both as an on-disk
35
+ * directory component (`<home>/projects/<name>/harness.overrides.yaml`,
36
+ * `resolvePaths`, `src/cli/loader.ts`) and as a value that will end up
37
+ * inside rendered CLI output? Two independent screens, both applied here
38
+ * at the SOURCE rather than at each consumer:
39
+ *
40
+ * - PATH ESCAPE. Rejects the empty string, `"."`, and `".."` (the two
41
+ * `path.join` special-cases that either no-op or climb a level,
42
+ * `".."` being exactly the shape a crafted, un-normalized `commondir`
43
+ * used to produce before the fix above), plus any name containing a
44
+ * forward slash or a backslash (Windows separator; POSIX permits a
45
+ * literal backslash IN a directory name, so this is not redundant
46
+ * with the platform's own path parsing).
47
+ * - CONTROL CHARACTERS. Rejects any name containing one of
48
+ * {@link PROJECT_NAME_CONTROL_CHARS}, NUL included (which is why
49
+ * there is no separate NUL clause). A newline would forge an extra,
50
+ * diagnostic-looking line of caller-chosen text wherever the value is
51
+ * rendered, an ESC would write raw terminal escapes into the
52
+ * operator's terminal, and a CR would overwrite the line it landed
53
+ * on. Screening here rather than at each rendering site is what makes
54
+ * "an accepted project name is a plain single-line string" a property
55
+ * of the VALUE: it also covers the surfaces that render the
56
+ * SUBSTITUTED memory directory path rather than the name itself
57
+ * (`harness doctor`'s "memory directory missing" line,
58
+ * `src/cli/doctor/format.ts`; `harness list memories`' `path` row
59
+ * field, `src/cli/list.ts`), which no per-site escaper of the name
60
+ * would ever have reached.
61
+ *
62
+ * A name that passes both screens may still not exist on disk
63
+ * (`resolvePaths` already handles that with `fs.existsSync`); this only
64
+ * guards against the value escaping the single path segment it is meant
65
+ * to occupy and against it carrying control characters anywhere
66
+ * downstream.
67
+ *
68
+ * WHAT A REJECTION LOOKS LIKE. `deriveProjectName` (`src/runtime/git-
69
+ * context.ts`) validates its own return value at every exit, so a
70
+ * checkout directory whose basename contains a control character now
71
+ * takes the same path a `".."` basename already took: the helper returns
72
+ * `null`, no project override layer is looked up for that repository,
73
+ * and the base/machine value applies unchanged. There is no error and no
74
+ * warning; the operator sees the host-wide value instead of a per-repo
75
+ * one (`harness doctor`'s setup-version finding then carries
76
+ * `projectName: null` and renders no `(project: X)` suffix). An explicit
77
+ * `--project` carrying a control character is instead reported:
78
+ * `resolvePaths` resolves no project layer for it, and
79
+ * `substituteProject` (`src/probes/memory.ts`) degrades to the "no
80
+ * project supplied" branch, which `harness doctor` surfaces as its
81
+ * rejected-`--project` warn line.
82
+ *
83
+ * Exported (task c88461c1, review round 3 residual; task `1c4eb3ea`
84
+ * of the batch-44 follow-up run) so `resolvePaths` (`src/cli/loader.ts`)
85
+ * can apply the SAME guard at its own `path.join` sink, defense in
86
+ * depth: this function already rejects an unsafe name at every
87
+ * `deriveProjectName` exit, but an `opts.project` reaching
88
+ * `resolvePaths`' OWN sink from anywhere else (a caller building
89
+ * `LoaderOptions` by hand, a future producer) had no equivalent check
90
+ * of its own until now. Applied a THIRD time at `substituteProject`'s
91
+ * own sink (`src/probes/memory.ts`, task `e904f25a`): an invalid name
92
+ * there degrades to the "no project supplied" branch instead of being
93
+ * interpolated into the `{project}` placeholder. Applied a FOURTH time
94
+ * at `buildLockEntries`' own sink (`src/io/harness-lock.ts`, task
95
+ * `b5e6ccb0`): an invalid name there skips the templated directory's
96
+ * lock entry instead of the raw value being interpolated into the path
97
+ * this function stats, hashes and writes into `harness.lock`.
98
+ * `resolvePaths`, `substituteProject` and `buildLockEntries` are now all
99
+ * guarded; one `{project}` sink is NOT: `generate-memory-index.ts`'s own
100
+ * substitution still interpolates an operator-supplied `--project` value
101
+ * into a path unvalidated, left unguarded (reached only via the explicit
102
+ * CLI flag, not this module's derivation; see CHANGELOG.md for the
103
+ * reservation/follow-up). That one call site calls this function
104
+ * nowhere, so it inherits NOTHING from the control-character screen
105
+ * above either: a caller reading this comment should not assume this
106
+ * function guards every `{project}`-shaped sink in the codebase, for
107
+ * path escapes or for control characters.
108
+ */
109
+ export function isValidProjectName(name) {
110
+ if (name.length === 0)
111
+ return false;
112
+ if (name === "." || name === "..")
113
+ return false;
114
+ if (PROJECT_NAME_CONTROL_CHARS.test(name))
115
+ return false;
116
+ return !name.includes("/") && !name.includes("\\");
117
+ }
118
+ /**
119
+ * Renders a REJECTED project name safe to interpolate into one line of
120
+ * CLI output. The invariant this helper is half of has two parts, and
121
+ * they are complementary rather than overlapping (task `e904f25a`):
122
+ *
123
+ * - An ACCEPTED name cannot carry a control character at all.
124
+ * {@link isValidProjectName} rejects C0, DEL and C1 at the source, so
125
+ * every value that survives validation is already a plain
126
+ * single-line string wherever it, or a path it was substituted into,
127
+ * is rendered. Nothing downstream of the validator has to enumerate
128
+ * its own rendering sites to stay safe.
129
+ * - A REJECTED name is by definition unvalidated, and it is still
130
+ * echoed back so the operator can see WHICH value was refused. Every
131
+ * such echo goes through this helper: `harness doctor`'s
132
+ * rejected-`--project` warn line and its per-directory note
133
+ * (`src/cli/doctor/format.ts`), doctor's header `project:` clause
134
+ * (which renders the raw `--project` whether it was accepted or
135
+ * rejected), and `harness list memories`' `project_rejected` row
136
+ * field (`src/cli/list.ts`, and with it the text table derived from
137
+ * it).
138
+ *
139
+ * The `session_start_preflight.setup` finding's `(project: X)` suffix is
140
+ * also wrapped, as defense in depth rather than as an echo of a rejected
141
+ * value: `harness doctor` sets that finding's `projectName` only when
142
+ * `resolvePaths` actually resolved a project layer FILE, which requires
143
+ * the name to have passed {@link isValidProjectName} first. The wrap
144
+ * stays so a hand-built finding or a future producer cannot reintroduce
145
+ * a forged line through that one call site.
146
+ *
147
+ * Strips (rather than escapes) every character in
148
+ * {@link PROJECT_NAME_CONTROL_CHARS}. A name with no control character
149
+ * (`..`, `a/b`, every ordinary shape) passes through unchanged. Report
150
+ * DATA keeps the raw value (`DoctorReport.project`,
151
+ * `MemoryReport.projectRejected`), so a `--json` consumer still reads
152
+ * back exactly what the operator passed, carried by JSON's own escaping
153
+ * rather than by a raw control byte.
154
+ */
155
+ export function sanitizeProjectForDisplay(name) {
156
+ return name.replace(PROJECT_NAME_CONTROL_CHARS_GLOBAL, "");
157
+ }
158
+ //# sourceMappingURL=project-name.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-name.js","sourceRoot":"","sources":["../../src/io/project-name.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,yCAAyC;AACzC,EAAE;AACF,4EAA4E;AAC5E,uEAAuE;AACvE,2EAA2E;AAC3E,+DAA+D;AAC/D,wEAAwE;AACxE,wEAAwE;AACxE,uEAAuE;AACvE,qEAAqE;AACrE,qEAAqE;AACrE,yEAAyE;AACzE,0EAA0E;AAC1E,uDAAuD;AAEvD;;;;;;;GAOG;AACH,MAAM,0BAA0B,GAAG,8BAA8B,CAAC;AAElE;;;;;;GAMG;AACH,MAAM,iCAAiC,GAAG,IAAI,MAAM,CAAC,0BAA0B,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE7F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAChD,IAAI,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACxD,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,OAAO,IAAI,CAAC,OAAO,CAAC,iCAAiC,EAAE,EAAE,CAAC,CAAC;AAC7D,CAAC"}
@@ -3,6 +3,16 @@ import * as path from "node:path";
3
3
  import { readRegularFileRejectingSymlink } from "./read-regular-file.js";
4
4
  import { safeJsonParse } from "./safe-json-parse.js";
5
5
  export function readJsonDirEntriesRejectingSymlinks(dir, opts) {
6
+ let dirStat;
7
+ try {
8
+ dirStat = fs.lstatSync(dir);
9
+ }
10
+ catch {
11
+ return { dirPresent: false, entries: [], unreadableCount: 0 };
12
+ }
13
+ if (!dirStat.isDirectory()) {
14
+ return { dirPresent: false, entries: [], unreadableCount: 0 };
15
+ }
6
16
  let dirents;
7
17
  try {
8
18
  dirents = fs.readdirSync(dir, { withFileTypes: true });
@@ -1 +1 @@
1
- {"version":3,"file":"read-json-dir-entries.js","sourceRoot":"","sources":["../../src/io/read-json-dir-entries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAuCrD,MAAM,UAAU,mCAAmC,CACjD,GAAW,EACX,IAAkC;IAElC,IAAI,OAAoB,CAAC;IACzB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC;IAChE,CAAC;IAED,MAAM,OAAO,GAAQ,EAAE,CAAC;IACxB,IAAI,eAAe,GAAG,CAAC,CAAC;IAExB,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC;QACpB,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC;YAAE,SAAS;QAEhC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,+BAA+B,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,aAAa,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACtF,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAC/B,eAAe,EAAE,CAAC;YAClB,SAAS;QACX,CAAC;QACD,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACvC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,eAAe,EAAE,CAAC;YAClB,SAAS;QACX,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC;AACxD,CAAC"}
1
+ {"version":3,"file":"read-json-dir-entries.js","sourceRoot":"","sources":["../../src/io/read-json-dir-entries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAuCrD,MAAM,UAAU,mCAAmC,CACjD,GAAW,EACX,IAAkC;IAElC,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC;IAChE,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;QAC3B,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC;IAChE,CAAC;IACD,IAAI,OAAoB,CAAC;IACzB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC;IAChE,CAAC;IAED,MAAM,OAAO,GAAQ,EAAE,CAAC;IACxB,IAAI,eAAe,GAAG,CAAC,CAAC;IAExB,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC;QACpB,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC;YAAE,SAAS;QAEhC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,+BAA+B,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,aAAa,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACtF,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAC/B,eAAe,EAAE,CAAC;YAClB,SAAS;QACX,CAAC;QACD,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACvC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,eAAe,EAAE,CAAC;YAClB,SAAS;QACX,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC;AACxD,CAAC"}
@@ -22,3 +22,62 @@
22
22
  export declare const NUMERIC_VERSION_PATTERN: RegExp;
23
23
  export declare const NUMERIC_VERSION_MESSAGE = "min_version must be numeric semver-shape: digits separated by up to three dots (e.g. \"1\", \"1.2\", \"1.2.3\", \"1.2.3.4\"). Pre-release suffixes and leading \"v\" are rejected.";
24
24
  export declare function compareNumericVersions(a: string, b: string): number;
25
+ /**
26
+ * Extracts a numeric-semver-shape version from free-form `--version`
27
+ * probe output (e.g. "preflight 0.6.0-rc.1\n"), plus whether the
28
+ * matched numeric run was immediately followed by a `-` prerelease/
29
+ * build suffix. Matches ONLY the leading numeric run itself (mirrors
30
+ * the pre-existing `/(\d+(?:\.\d+){0,3})/` extraction every version
31
+ * floor check used before task 65952a0c), so `version` here is exactly
32
+ * what a plain, non-prerelease-aware caller would have parsed; the
33
+ * `isPrerelease` flag is the new information a caller can act on.
34
+ *
35
+ * Returns `null` when no numeric run is found at all (unparseable
36
+ * probe output), matching the pre-existing "no match" contract.
37
+ *
38
+ * Two suffix strings are returned, deliberately different:
39
+ * - `raw` is the regex's own match (`m[0]`): the numeric run plus a
40
+ * `-` suffix restricted to `[0-9A-Za-z.]`, so it TRUNCATES a suffix
41
+ * containing any other character (a git-describe suffix's second
42
+ * hyphen, e.g. "0.6.0-4-gabc123" matches only as far as "0.6.0-4";
43
+ * a platform suffix, e.g. "0.6.0-linux-x64" matches only as far as
44
+ * "0.6.0-linux"). Kept only because `isPrerelease` is derived from
45
+ * whether `m[2]` (the restricted suffix group) matched at all.
46
+ * - `token` is the full probed version token as printed: everything
47
+ * from the start of the numeric run up to the next whitespace
48
+ * character (or end of string), with no character-class
49
+ * restriction. This is what a human-facing message should quote;
50
+ * `raw`'s truncation would otherwise misreport what the probe
51
+ * actually printed. See docs/decisions/2026-09-08-preflight-floors.md.
52
+ */
53
+ export declare function parseProbedVersion(stdout: string): {
54
+ version: string;
55
+ isPrerelease: boolean;
56
+ raw: string;
57
+ token: string;
58
+ } | null;
59
+ /**
60
+ * `compareNumericVersions`, but a prerelease `a` (as reported by
61
+ * `parseProbedVersion`'s `isPrerelease`) is treated as strictly BELOW
62
+ * a numerically-equal `b` floor, matching semver precedence
63
+ * (`0.6.0-rc.1 < 0.6.0`) and the intent every `min_version` floor in
64
+ * this codebase already carries: `NUMERIC_VERSION_PATTERN` rejects a
65
+ * prerelease shape for `b` itself (a `min_version` field can never
66
+ * BE a prerelease), so `aIsPrerelease` is the only side this can ever
67
+ * apply to. When the base numeric comparison is not a tie, the
68
+ * prerelease flag is irrelevant and the numeric result wins outright
69
+ * (a genuinely older release, prerelease or not, is still older).
70
+ *
71
+ * Decision: docs/decisions/2026-09-08-preflight-floors.md (see the
72
+ * Reopen criteria section for task db44ab46, which extended the rule
73
+ * from its original two consumers to all `min_version` floor checks in
74
+ * this codebase). Consumers as of task db44ab46: `checkHookVersion` and
75
+ * `checkCli`'s `tools.cli[]` check plus `checkMcpVersions`'s
76
+ * `tools.mcp[]` check (all in src/cli/doctor/index.ts),
77
+ * `checkSessionStartPreflightSetupVersion`
78
+ * (src/cli/doctor/session-start-preflight-setup-version.ts), validate's
79
+ * `tools.cli[]` check (src/cli/validate/checks.ts), `memory.router`'s
80
+ * version probe (src/probes/memory.ts), and the pack-level floor
81
+ * (src/policy-packs/version-check.ts).
82
+ */
83
+ export declare function compareVersionFloor(a: string, aIsPrerelease: boolean, b: string): number;
@@ -37,4 +37,71 @@ export function compareNumericVersions(a, b) {
37
37
  }
38
38
  return 0;
39
39
  }
40
+ /**
41
+ * Extracts a numeric-semver-shape version from free-form `--version`
42
+ * probe output (e.g. "preflight 0.6.0-rc.1\n"), plus whether the
43
+ * matched numeric run was immediately followed by a `-` prerelease/
44
+ * build suffix. Matches ONLY the leading numeric run itself (mirrors
45
+ * the pre-existing `/(\d+(?:\.\d+){0,3})/` extraction every version
46
+ * floor check used before task 65952a0c), so `version` here is exactly
47
+ * what a plain, non-prerelease-aware caller would have parsed; the
48
+ * `isPrerelease` flag is the new information a caller can act on.
49
+ *
50
+ * Returns `null` when no numeric run is found at all (unparseable
51
+ * probe output), matching the pre-existing "no match" contract.
52
+ *
53
+ * Two suffix strings are returned, deliberately different:
54
+ * - `raw` is the regex's own match (`m[0]`): the numeric run plus a
55
+ * `-` suffix restricted to `[0-9A-Za-z.]`, so it TRUNCATES a suffix
56
+ * containing any other character (a git-describe suffix's second
57
+ * hyphen, e.g. "0.6.0-4-gabc123" matches only as far as "0.6.0-4";
58
+ * a platform suffix, e.g. "0.6.0-linux-x64" matches only as far as
59
+ * "0.6.0-linux"). Kept only because `isPrerelease` is derived from
60
+ * whether `m[2]` (the restricted suffix group) matched at all.
61
+ * - `token` is the full probed version token as printed: everything
62
+ * from the start of the numeric run up to the next whitespace
63
+ * character (or end of string), with no character-class
64
+ * restriction. This is what a human-facing message should quote;
65
+ * `raw`'s truncation would otherwise misreport what the probe
66
+ * actually printed. See docs/decisions/2026-09-08-preflight-floors.md.
67
+ */
68
+ export function parseProbedVersion(stdout) {
69
+ const m = stdout.match(/(\d+(?:\.\d+){0,3})(-[0-9A-Za-z.]+)?/);
70
+ if (!m || !m[1] || m.index === undefined)
71
+ return null;
72
+ const rest = stdout.slice(m.index);
73
+ const wsIndex = rest.search(/\s/);
74
+ const token = wsIndex === -1 ? rest : rest.slice(0, wsIndex);
75
+ return { version: m[1], isPrerelease: m[2] !== undefined, raw: m[0], token };
76
+ }
77
+ /**
78
+ * `compareNumericVersions`, but a prerelease `a` (as reported by
79
+ * `parseProbedVersion`'s `isPrerelease`) is treated as strictly BELOW
80
+ * a numerically-equal `b` floor, matching semver precedence
81
+ * (`0.6.0-rc.1 < 0.6.0`) and the intent every `min_version` floor in
82
+ * this codebase already carries: `NUMERIC_VERSION_PATTERN` rejects a
83
+ * prerelease shape for `b` itself (a `min_version` field can never
84
+ * BE a prerelease), so `aIsPrerelease` is the only side this can ever
85
+ * apply to. When the base numeric comparison is not a tie, the
86
+ * prerelease flag is irrelevant and the numeric result wins outright
87
+ * (a genuinely older release, prerelease or not, is still older).
88
+ *
89
+ * Decision: docs/decisions/2026-09-08-preflight-floors.md (see the
90
+ * Reopen criteria section for task db44ab46, which extended the rule
91
+ * from its original two consumers to all `min_version` floor checks in
92
+ * this codebase). Consumers as of task db44ab46: `checkHookVersion` and
93
+ * `checkCli`'s `tools.cli[]` check plus `checkMcpVersions`'s
94
+ * `tools.mcp[]` check (all in src/cli/doctor/index.ts),
95
+ * `checkSessionStartPreflightSetupVersion`
96
+ * (src/cli/doctor/session-start-preflight-setup-version.ts), validate's
97
+ * `tools.cli[]` check (src/cli/validate/checks.ts), `memory.router`'s
98
+ * version probe (src/probes/memory.ts), and the pack-level floor
99
+ * (src/policy-packs/version-check.ts).
100
+ */
101
+ export function compareVersionFloor(a, aIsPrerelease, b) {
102
+ const cmp = compareNumericVersions(a, b);
103
+ if (cmp !== 0)
104
+ return cmp;
105
+ return aIsPrerelease ? -1 : 0;
106
+ }
40
107
  //# sourceMappingURL=version-compare.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version-compare.js","sourceRoot":"","sources":["../../src/io/version-compare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,qBAAqB,CAAC;AAE7D,MAAM,CAAC,MAAM,uBAAuB,GAClC,0KAA0K,CAAC;AAE7K,MAAM,UAAU,sBAAsB,CAAC,CAAS,EAAE,CAAS;IACzD,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC3D,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACtB,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YAAE,OAAO,CAAC,CAAC;QACnD,IAAI,EAAE,GAAG,EAAE;YAAE,OAAO,CAAC,CAAC;QACtB,IAAI,EAAE,GAAG,EAAE;YAAE,OAAO,CAAC,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"}
1
+ {"version":3,"file":"version-compare.js","sourceRoot":"","sources":["../../src/io/version-compare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,qBAAqB,CAAC;AAE7D,MAAM,CAAC,MAAM,uBAAuB,GAClC,0KAA0K,CAAC;AAE7K,MAAM,UAAU,sBAAsB,CAAC,CAAS,EAAE,CAAS;IACzD,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC3D,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACtB,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YAAE,OAAO,CAAC,CAAC;QACnD,IAAI,EAAE,GAAG,EAAE;YAAE,OAAO,CAAC,CAAC;QACtB,IAAI,EAAE,GAAG,EAAE;YAAE,OAAO,CAAC,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAAc;IAEd,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC/D,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACtD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC7D,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;AAC/E,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,mBAAmB,CAAC,CAAS,EAAE,aAAsB,EAAE,CAAS;IAC9E,MAAM,GAAG,GAAG,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACzC,IAAI,GAAG,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAC1B,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChC,CAAC"}
@@ -0,0 +1,50 @@
1
+ /** The grounding-mcp version this reconnect lifecycle was verified against. */
2
+ export declare const RECONNECT_PRODUCER_FLOOR = "grounding-mcp >= 0.11.0";
3
+ /** Rendered qualifier prefix both surfaces open the guidance with. */
4
+ export declare const RECONNECT_VERSION_QUALIFIER = "With grounding-mcp >= 0.11.0:";
5
+ export declare const RECONNECT_STATUS_TOOL = "mcp__grounding-mcp__solution_evaluate_status";
6
+ export declare const RECONNECT_RESULT_TOOL = "mcp__grounding-mcp__solution_evaluate_result";
7
+ export declare const RECONNECT_TASK_ID_PLACEHOLDER = "<task-id>";
8
+ export declare const RECONNECT_POLL_MS_ADVERTISED = "5000ms";
9
+ export declare const RECONNECT_RETENTION = "24h";
10
+ export declare const RECONNECT_RETENTION_FLOOR = "100x pollAfterMs";
11
+ /**
12
+ * The three readings `gate.verdict === null` covers. The hook
13
+ * (`classifyNullVerdictReading` / `readAttemptLockLiveness` in
14
+ * `../../cli/pack/hook-solution-acceptance.ts`) reads the documented
15
+ * attempt-lock anchor to rule two of the three out and select the one it
16
+ * detected; this module only owns the wording listing them, consumed by
17
+ * `renderReconnectDenyParagraph` below for its reading-(2)-only paragraph.
18
+ */
19
+ export declare const RECONNECT_THREE_READINGS_LABELS: readonly ["`solution_evaluate` was never called for \"<task-id>\"", "a `solution_evaluate` call for it is still running in the background", "a marker path exists but what is there was not accepted as a verdict"];
20
+ /** Fact 1: reconnect by attempt id, not by starting a fresh call. */
21
+ export declare const RECONNECT_FACT_RECONNECT_BY_ID: string;
22
+ /**
23
+ * Fact 2: never retry while the lock is held; a second call JOINS, it does
24
+ * not refuse. It deliberately begins lowercase because each renderer owns
25
+ * its own lead-in sentence.
26
+ */
27
+ export declare const RECONNECT_FACT_JOIN_NOT_RETRY: string;
28
+ /**
29
+ * Fact 3: poll interval and retention bounds, from the released
30
+ * grounding-mcp version. Both renderers use this fact as a fresh sentence
31
+ * (after a period), so it is capitalized.
32
+ */
33
+ export declare const RECONNECT_FACT_POLL_AND_RETENTION: string;
34
+ /**
35
+ * Compact paragraph appended to the completion-gate's deny reason
36
+ * (`blockJson` in hook-solution-acceptance.ts) when `gate.verdict ===
37
+ * null` AND the hook detected reading (2), a live attempt-lock
38
+ * (`reconnectGuidanceFor`). Renders the SAME fact constants as
39
+ * `renderReconnectInstructionsSection` below.
40
+ */
41
+ export declare function renderReconnectDenyParagraph(taskId: string): string;
42
+ /**
43
+ * "Reconnecting vs. retrying" section body rendered into the pack's
44
+ * `instructions.md` operator audit copy (`buildInstructions` in
45
+ * solution-acceptance.ts). Renders the SAME fact constants as
46
+ * `renderReconnectDenyParagraph` above; a large repo can outlive a single
47
+ * `solution_evaluate` call, so this section documents reconnecting to an
48
+ * in-flight attempt rather than treating a timeout as a stall to retry.
49
+ */
50
+ export declare function renderReconnectInstructionsSection(): string;
@@ -0,0 +1,151 @@
1
+ // Reconnect-vs-retry facts for the `solution-acceptance` pack (task
2
+ // 5c9cad05), owned as DATA in exactly one place so the two rendered
3
+ // surfaces cannot drift apart the way they did across review rounds 1-2:
4
+ //
5
+ // 1. `buildInstructions` (solution-acceptance.ts) renders the operator
6
+ // audit copy's "Reconnecting vs. retrying" section from
7
+ // `renderReconnectInstructionsSection`.
8
+ // 2. `blockJson` (../../cli/pack/hook-solution-acceptance.ts) renders the
9
+ // agent-facing deny paragraph from `renderReconnectDenyParagraph`,
10
+ // shown only when `gate.verdict === null` AND the hook detected
11
+ // reading (2), a live attempt-lock (see `reconnectGuidanceFor` in
12
+ // hook-solution-acceptance.ts; the three readings are listed in
13
+ // `RECONNECT_THREE_READINGS_LABELS`
14
+ // below).
15
+ //
16
+ // Both renderers are built from the SAME fact constants below (not just
17
+ // the same values restated in prose twice), and
18
+ // `tests/policy-packs/solution-acceptance-reconnect.test.ts` asserts each
19
+ // fact constant appears verbatim in both rendered surfaces, so a future
20
+ // edit that updates one surface but not the other fails that test instead
21
+ // of shipping a silent drift.
22
+ //
23
+ // Scope: this module documents grounding-mcp's ATTEMPT lifecycle
24
+ // (`solution_evaluate_status` / `solution_evaluate_result`, `attemptId`,
25
+ // `pollAfterMs`, retention), which the pack's producer floor (>= 0.3.2)
26
+ // does not itself guarantee (that lifecycle shipped in grounding-mcp
27
+ // 0.11.0), so every rendering below is explicitly qualified with
28
+ // `RECONNECT_VERSION_QUALIFIER` rather than stated as if it held
29
+ // unconditionally under the pack's own (older) producer floor.
30
+ //
31
+ // grounding-mcp's README also documents an attempt-lock anchor
32
+ // (`<verdict dir>/<id>.attempt-lock`, beside the
33
+ // `<id>.attempt-lock.lock` directory `proper-lockfile` manages, from
34
+ // which the producer itself derives its own `running-unconfirmed`
35
+ // status). The anchor IS read: `classifyNullVerdictReading` /
36
+ // `readAttemptLockLiveness` in `../../cli/pack/hook-solution-acceptance.ts`
37
+ // read it to rule two of the three readings below out and select the
38
+ // one it detected (harness/799de976). This module does not touch the
39
+ // filesystem itself; it only owns the WORDING both rendered surfaces
40
+ // share, including `renderReconnectDenyParagraph`'s reading-(2)-only
41
+ // paragraph below, which the hook appends solely when it has detected
42
+ // the live-attempt reading.
43
+ /** The grounding-mcp version this reconnect lifecycle was verified against. */
44
+ export const RECONNECT_PRODUCER_FLOOR = "grounding-mcp >= 0.11.0";
45
+ /** Rendered qualifier prefix both surfaces open the guidance with. */
46
+ export const RECONNECT_VERSION_QUALIFIER = `With ${RECONNECT_PRODUCER_FLOOR}:`;
47
+ export const RECONNECT_STATUS_TOOL = "mcp__grounding-mcp__solution_evaluate_status";
48
+ export const RECONNECT_RESULT_TOOL = "mcp__grounding-mcp__solution_evaluate_result";
49
+ export const RECONNECT_TASK_ID_PLACEHOLDER = "<task-id>";
50
+ export const RECONNECT_POLL_MS_ADVERTISED = "5000ms";
51
+ export const RECONNECT_RETENTION = "24h";
52
+ export const RECONNECT_RETENTION_FLOOR = "100x pollAfterMs";
53
+ /**
54
+ * The three readings `gate.verdict === null` covers. The hook
55
+ * (`classifyNullVerdictReading` / `readAttemptLockLiveness` in
56
+ * `../../cli/pack/hook-solution-acceptance.ts`) reads the documented
57
+ * attempt-lock anchor to rule two of the three out and select the one it
58
+ * detected; this module only owns the wording listing them, consumed by
59
+ * `renderReconnectDenyParagraph` below for its reading-(2)-only paragraph.
60
+ */
61
+ export const RECONNECT_THREE_READINGS_LABELS = [
62
+ `\`solution_evaluate\` was never called for "${RECONNECT_TASK_ID_PLACEHOLDER}"`,
63
+ "a `solution_evaluate` call for it is still running in the background",
64
+ "a marker path exists but what is there was not accepted as a verdict",
65
+ ];
66
+ /** Fact 1: reconnect by attempt id, not by starting a fresh call. */
67
+ export const RECONNECT_FACT_RECONNECT_BY_ID = `poll \`${RECONNECT_STATUS_TOOL}\` /\n` +
68
+ `\`${RECONNECT_RESULT_TOOL}\` for the SAME \`id\`,\n` +
69
+ `passing the \`attemptId\` you were given (or omitting it to resolve the latest attempt,\n` +
70
+ `the recovery path when your own call timed out before it ever returned one)`;
71
+ /**
72
+ * Fact 2: never retry while the lock is held; a second call JOINS, it does
73
+ * not refuse. It deliberately begins lowercase because each renderer owns
74
+ * its own lead-in sentence.
75
+ */
76
+ export const RECONNECT_FACT_JOIN_NOT_RETRY = `a second \`solution_evaluate\` call for an \`id\` whose attempt is still live just joins\n` +
77
+ `that attempt and returns its \`attemptId\`, never starting a second \`preflight\` run;\n` +
78
+ `only \`forceNewAttempt\` is refused while that attempt's lock holds`;
79
+ /**
80
+ * Fact 3: poll interval and retention bounds, from the released
81
+ * grounding-mcp version. Both renderers use this fact as a fresh sentence
82
+ * (after a period), so it is capitalized.
83
+ */
84
+ export const RECONNECT_FACT_POLL_AND_RETENTION = `Wait at least the returned \`pollAfterMs\` (advertised as \`${RECONNECT_POLL_MS_ADVERTISED}\`)\n` +
85
+ `between polls; attempt records are retained \`${RECONNECT_RETENTION}\` by default (always at\n` +
86
+ `least \`${RECONNECT_RETENTION_FLOOR}\`), and a pruned terminal attempt\n` +
87
+ `reads \`expired\``;
88
+ /** The three readings, as one clause, with the task id interpolated. */
89
+ function threeReadingsClause(taskId) {
90
+ const [neverCalled, stillRunning, unreadable] = RECONNECT_THREE_READINGS_LABELS;
91
+ return [
92
+ neverCalled.replace(RECONNECT_TASK_ID_PLACEHOLDER, taskId),
93
+ stillRunning,
94
+ `or ${unreadable}`,
95
+ ].join(",\n");
96
+ }
97
+ /**
98
+ * Compact paragraph appended to the completion-gate's deny reason
99
+ * (`blockJson` in hook-solution-acceptance.ts) when `gate.verdict ===
100
+ * null` AND the hook detected reading (2), a live attempt-lock
101
+ * (`reconnectGuidanceFor`). Renders the SAME fact constants as
102
+ * `renderReconnectInstructionsSection` below.
103
+ */
104
+ export function renderReconnectDenyParagraph(taskId) {
105
+ return `\n${[
106
+ `Reconnecting vs. retrying. ${RECONNECT_VERSION_QUALIFIER} this same`,
107
+ `"no readable verdict marker" case covers three readings:`,
108
+ threeReadingsClause(taskId),
109
+ `(the verdict marker only appears once an attempt finishes, and is validated on read;`,
110
+ `this hook read the documented attempt-lock anchor and is showing you this paragraph`,
111
+ `because it detected reading (2): an attempt is still live). If you already called`,
112
+ `\`solution_evaluate\` for this \`id\`, do not call it again:`,
113
+ `${RECONNECT_FACT_RECONNECT_BY_ID}.`,
114
+ `Never re-call \`solution_evaluate\`:`,
115
+ `${RECONNECT_FACT_JOIN_NOT_RETRY}.`,
116
+ `${RECONNECT_FACT_POLL_AND_RETENTION}.`,
117
+ ].join("\n")}\n`;
118
+ }
119
+ /**
120
+ * "Reconnecting vs. retrying" section body rendered into the pack's
121
+ * `instructions.md` operator audit copy (`buildInstructions` in
122
+ * solution-acceptance.ts). Renders the SAME fact constants as
123
+ * `renderReconnectDenyParagraph` above; a large repo can outlive a single
124
+ * `solution_evaluate` call, so this section documents reconnecting to an
125
+ * in-flight attempt rather than treating a timeout as a stall to retry.
126
+ */
127
+ export function renderReconnectInstructionsSection() {
128
+ return `## Reconnecting vs. retrying (${RECONNECT_PRODUCER_FLOOR})
129
+
130
+ ${RECONNECT_VERSION_QUALIFIER} a large repo can outlive the call: \`solution_evaluate\` may
131
+ hand back \`{status: "running", attemptId, id, pollAfterMs}\` instead of a verdict, or your
132
+ own call may time out with nothing at all. Either way the \`preflight\` run keeps going in
133
+ the background; reconnect as follows:
134
+ ${RECONNECT_FACT_RECONNECT_BY_ID}.
135
+
136
+ Never re-call \`solution_evaluate\` as a stall workaround:
137
+ ${RECONNECT_FACT_JOIN_NOT_RETRY}.
138
+ A prior attempt's reported status (\`completed\`,
139
+ \`failed\`, \`unknown\`, or \`expired\`) is informational, not the gate: \`unknown\`/
140
+ \`expired\` never license a new attempt by themselves while another process still holds the
141
+ id's lock. A genuinely new attempt becomes possible only once the previous one is terminal and
142
+ the id's lock is free again, at which point an ordinary \`solution_evaluate\` call starts
143
+ one.
144
+
145
+ ${RECONNECT_FACT_POLL_AND_RETENTION}. \`running-unconfirmed\` still means keep
146
+ polling, not stall or escalate: it resolves by itself, into \`running\`
147
+ or once the lock is reclaimed as stale. Never escalate to a human before
148
+ the poll hint has elapsed.
149
+ `;
150
+ }
151
+ //# sourceMappingURL=solution-acceptance-reconnect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solution-acceptance-reconnect.js","sourceRoot":"","sources":["../../../src/policy-packs/builtin/solution-acceptance-reconnect.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,oEAAoE;AACpE,yEAAyE;AACzE,EAAE;AACF,yEAAyE;AACzE,6DAA6D;AAC7D,6CAA6C;AAC7C,4EAA4E;AAC5E,wEAAwE;AACxE,qEAAqE;AACrE,uEAAuE;AACvE,qEAAqE;AACrE,yCAAyC;AACzC,eAAe;AACf,EAAE;AACF,wEAAwE;AACxE,gDAAgD;AAChD,0EAA0E;AAC1E,wEAAwE;AACxE,0EAA0E;AAC1E,8BAA8B;AAC9B,EAAE;AACF,iEAAiE;AACjE,yEAAyE;AACzE,wEAAwE;AACxE,qEAAqE;AACrE,iEAAiE;AACjE,iEAAiE;AACjE,+DAA+D;AAC/D,EAAE;AACF,+DAA+D;AAC/D,iDAAiD;AACjD,qEAAqE;AACrE,kEAAkE;AAClE,8DAA8D;AAC9D,4EAA4E;AAC5E,qEAAqE;AACrE,qEAAqE;AACrE,qEAAqE;AACrE,qEAAqE;AACrE,sEAAsE;AACtE,4BAA4B;AAE5B,+EAA+E;AAC/E,MAAM,CAAC,MAAM,wBAAwB,GAAG,yBAAyB,CAAC;AAElE,sEAAsE;AACtE,MAAM,CAAC,MAAM,2BAA2B,GAAG,QAAQ,wBAAwB,GAAG,CAAC;AAE/E,MAAM,CAAC,MAAM,qBAAqB,GAAG,8CAA8C,CAAC;AACpF,MAAM,CAAC,MAAM,qBAAqB,GAAG,8CAA8C,CAAC;AACpF,MAAM,CAAC,MAAM,6BAA6B,GAAG,WAAW,CAAC;AAEzD,MAAM,CAAC,MAAM,4BAA4B,GAAG,QAAQ,CAAC;AACrD,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC;AACzC,MAAM,CAAC,MAAM,yBAAyB,GAAG,kBAAkB,CAAC;AAE5D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG;IAC7C,+CAA+C,6BAA6B,GAAG;IAC/E,sEAAsE;IACtE,sEAAsE;CAC9D,CAAC;AAEX,qEAAqE;AACrE,MAAM,CAAC,MAAM,8BAA8B,GACzC,UAAU,qBAAqB,QAAQ;IACvC,KAAK,qBAAqB,2BAA2B;IACrD,2FAA2F;IAC3F,6EAA6E,CAAC;AAEhF;;;;GAIG;AACH,MAAM,CAAC,MAAM,6BAA6B,GACxC,4FAA4F;IAC5F,0FAA0F;IAC1F,qEAAqE,CAAC;AAExE;;;;GAIG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAC5C,+DAA+D,4BAA4B,OAAO;IAClG,iDAAiD,mBAAmB,4BAA4B;IAChG,WAAW,yBAAyB,sCAAsC;IAC1E,mBAAmB,CAAC;AAEtB,wEAAwE;AACxE,SAAS,mBAAmB,CAAC,MAAc;IACzC,MAAM,CAAC,WAAW,EAAE,YAAY,EAAE,UAAU,CAAC,GAAG,+BAA+B,CAAC;IAChF,OAAO;QACL,WAAW,CAAC,OAAO,CAAC,6BAA6B,EAAE,MAAM,CAAC;QAC1D,YAAY;QACZ,MAAM,UAAU,EAAE;KACnB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAc;IACzD,OAAO,KAAK;QACV,8BAA8B,2BAA2B,YAAY;QACrE,0DAA0D;QAC1D,mBAAmB,CAAC,MAAM,CAAC;QAC3B,sFAAsF;QACtF,qFAAqF;QACrF,mFAAmF;QACnF,8DAA8D;QAC9D,GAAG,8BAA8B,GAAG;QACpC,sCAAsC;QACtC,GAAG,6BAA6B,GAAG;QACnC,GAAG,iCAAiC,GAAG;KACxC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACnB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kCAAkC;IAChD,OAAO,iCAAiC,wBAAwB;;EAEhE,2BAA2B;;;;EAI3B,8BAA8B;;;EAG9B,6BAA6B;;;;;;;;EAQ7B,iCAAiC;;;;CAIlC,CAAC;AACF,CAAC"}
@@ -170,12 +170,51 @@ export declare function verdictPathFor(dir: string, id: string): string;
170
170
  * active-claim is present.
171
171
  */
172
172
  export declare function resolveExplicitVerdictId(env?: NodeJS.ProcessEnv): string | null;
173
+ /**
174
+ * WHY the verdict marker for an id yielded no verdict, as ONE observation.
175
+ * `readVerdict` below answers `null` for every non-`ok` kind, which is all
176
+ * the gate DECISION needs; the deny TEXT needs to say which one, and must
177
+ * not learn it from a second look at the path. A second look can disagree
178
+ * with the first (a background attempt finishing in between is exactly the
179
+ * case this pack exists for), and a note rendered from the later one then
180
+ * describes a rejection the gate never made: it would tell the agent its
181
+ * fresh marker was refused when the gate had simply not seen it yet
182
+ * (review finding, harness/799de976, round 6). Both consumers therefore
183
+ * read through this one function.
184
+ */
185
+ export type VerdictReadOutcome = {
186
+ kind: "ok";
187
+ verdict: Verdict;
188
+ } | {
189
+ kind: "invalid-id";
190
+ } | {
191
+ kind: "missing";
192
+ } | {
193
+ kind: "symlink";
194
+ } | {
195
+ kind: "not-regular";
196
+ } | {
197
+ kind: "unreadable";
198
+ } | {
199
+ kind: "invalid-record";
200
+ };
201
+ /**
202
+ * Read + validate the verdict marker for `id` in ONE filesystem
203
+ * observation, reporting the outcome instead of collapsing it. The
204
+ * symlink-rejecting read is the shared `src/io/read-regular-file.ts`
205
+ * helper (same defense-in-depth as `checkApprovalMarker` against a symlink
206
+ * planted at the marker path pointing at agent-controlled content), and
207
+ * its four non-ok kinds pass straight through, so this outcome set cannot
208
+ * drift from the reader's own. `invalid-record` is the one kind added on
209
+ * top: a regular file that WAS read but is not a verdict (unparseable
210
+ * JSON, or JSON missing one of the three required fields).
211
+ */
212
+ export declare function readVerdictDetailed(dir: string, id: string): VerdictReadOutcome;
173
213
  /**
174
214
  * Read + validate the verdict marker for `id`, or null when it is absent,
175
- * unparseable, a symlink, or not a regular file. The symlink-rejecting read
176
- * is the shared `src/io/read-regular-file.ts` helper (same defense-in-depth
177
- * as `checkApprovalMarker` against a symlink planted at the marker path
178
- * pointing at agent-controlled content).
215
+ * unparseable, a symlink, or not a regular file: `readVerdictDetailed`
216
+ * above with the reason dropped. Behaviour is unchanged for every caller
217
+ * that only needs the gate decision.
179
218
  */
180
219
  export declare function readVerdict(dir: string, id: string): Verdict | null;
181
220
  export interface GateResult {