@infinitedusky/indusk-mcp 1.44.0 → 1.44.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/.next/BUILD_ID +1 -1
- package/admin/.next/build-manifest.json +3 -3
- package/admin/.next/fallback-build-manifest.json +3 -3
- package/admin/.next/prerender-manifest.json +3 -3
- package/admin/.next/server/app/_global-error.html +1 -1
- package/admin/.next/server/app/_global-error.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/admin/.next/server/middleware-build-manifest.js +3 -3
- package/admin/.next/server/pages/500.html +1 -1
- package/admin/.next/server/server-reference-manifest.js +1 -1
- package/admin/.next/server/server-reference-manifest.json +1 -1
- package/admin/.next/trace +1 -1
- package/admin/.next/trace-build +1 -1
- package/dist/bin/commands/agent.js +3 -3
- package/dist/bin/commands/extensions.d.ts +11 -0
- package/dist/bin/commands/extensions.js +10 -0
- package/dist/bin/commands/run.js +21 -0
- package/dist/bin/commands/update.js +18 -14
- package/dist/bin/commands/workbench.d.ts +0 -8
- package/dist/bin/commands/workbench.js +28 -10
- package/dist/bin/commands/worktree.d.ts +10 -0
- package/dist/bin/commands/worktree.js +12 -5
- package/dist/lib/cleanup/oversized.js +16 -4
- package/dist/lib/eval/evaluator-runner.d.ts +3 -2
- package/dist/lib/stray-state-audit.js +5 -2
- package/dist/lib/trajectory/state-ops.d.ts +0 -6
- package/dist/lib/trajectory/state-ops.js +0 -15
- package/dist/lib/verify/roots.js +5 -2
- package/dist/lib/worktree/layout.js +9 -3
- package/dist/lib/worktree/repos.d.ts +10 -1
- package/dist/lib/worktree/repos.js +13 -2
- package/dist/lib/worktree/shareable.d.ts +3 -1
- package/dist/lib/worktree/shareable.js +53 -5
- package/extensions/worktree/hooks/on_enable.sh +17 -4
- package/extensions/worktree/manifest.json +1 -0
- package/extensions/worktree/scripts/lib/workbench-helpers.sh +58 -4
- package/extensions/worktree/scripts/preflight.sh +10 -46
- package/extensions/worktree/scripts/refresh-worktree.sh +18 -10
- package/extensions/worktree/scripts/setup-worktree.sh +11 -3
- package/extensions/worktree/templates/worktree-config.template.json +1 -1
- package/hooks/_hook-paths.js +129 -17
- package/hooks/_impl-phases.js +88 -0
- package/hooks/check-gates.js +3 -70
- package/hooks/eval-trigger.js +31 -5
- package/hooks/gate-reminder.js +92 -187
- package/package.json +2 -2
- package/skills/cleanup.md +1 -1
- package/skills/retrospective.md +1 -0
- /package/admin/.next/static/{4Mqj7sc33tNR898aibslD → F21UYqHRn4_drSf3X1wvn}/_buildManifest.js +0 -0
- /package/admin/.next/static/{4Mqj7sc33tNR898aibslD → F21UYqHRn4_drSf3X1wvn}/_clientMiddlewareManifest.js +0 -0
- /package/admin/.next/static/{4Mqj7sc33tNR898aibslD → F21UYqHRn4_drSf3X1wvn}/_ssgManifest.js +0 -0
|
@@ -61,55 +61,19 @@ if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
|
61
61
|
exit 1
|
|
62
62
|
fi
|
|
63
63
|
|
|
64
|
-
#
|
|
65
|
-
|
|
64
|
+
# One resolution surface — the root and every declared worktrees dir, exact
|
|
65
|
+
# match first, `-<slug>` suffix as fallback, ambiguity refused by name — the
|
|
66
|
+
# same resolver behind `wt` (workbench-trust-fixes, F5). The private root-only
|
|
67
|
+
# scan that stood here could not see a declared worktrees dir and carried its
|
|
68
|
+
# own reserved-name list, already drifted from the shared one.
|
|
69
|
+
WORKTREE_PATH="$(_wt_resolve_target "$SLUG")" || exit 1
|
|
70
|
+
TRUNK_DIR="$(_wt_resolve_trunk_dir "$REPO")" || exit 1
|
|
71
|
+
# Reject preflighting the trunk — preflight is a feature-branch concept. By
|
|
72
|
+
# resolved path: under a declared `path` the trunk's basename is not the name.
|
|
73
|
+
if [[ "$(cd "$WORKTREE_PATH" && pwd -P)" == "$(cd "$TRUNK_DIR" && pwd -P)" ]]; then
|
|
66
74
|
echo "Error: preflight cannot target the trunk; pick a worktree slug" >&2
|
|
67
75
|
exit 1
|
|
68
76
|
fi
|
|
69
|
-
|
|
70
|
-
# Slug resolution (single-pass against workbench root; same as wt.sh).
|
|
71
|
-
_is_reserved_name() {
|
|
72
|
-
case "$1" in
|
|
73
|
-
.indusk | .claude | .vscode | .cursor | node_modules | dist | build | .git | .next | scripts | env) return 0 ;;
|
|
74
|
-
*) return 1 ;;
|
|
75
|
-
esac
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
exact_paths=()
|
|
79
|
-
suffix_paths=()
|
|
80
|
-
for entry in "$WORKBENCH_ROOT"/*; do
|
|
81
|
-
[[ -d "$entry" ]] || continue
|
|
82
|
-
name="$(basename "$entry")"
|
|
83
|
-
_is_reserved_name "$name" && continue
|
|
84
|
-
# Skip the trunk symlink — preflight is for feature worktrees.
|
|
85
|
-
[[ "$name" == "$REPO" ]] && continue
|
|
86
|
-
if [[ "$name" == "$SLUG" ]]; then
|
|
87
|
-
exact_paths+=("$entry")
|
|
88
|
-
elif [[ "$name" == *"-$SLUG" ]]; then
|
|
89
|
-
suffix_paths+=("$entry")
|
|
90
|
-
fi
|
|
91
|
-
done
|
|
92
|
-
|
|
93
|
-
candidate_paths=()
|
|
94
|
-
if [[ ${#exact_paths[@]} -gt 0 ]]; then
|
|
95
|
-
candidate_paths=("${exact_paths[@]}")
|
|
96
|
-
elif [[ ${#suffix_paths[@]} -gt 0 ]]; then
|
|
97
|
-
candidate_paths=("${suffix_paths[@]}")
|
|
98
|
-
fi
|
|
99
|
-
|
|
100
|
-
if [[ ${#candidate_paths[@]} -eq 0 ]]; then
|
|
101
|
-
echo "Error: no worktree matching slug '$SLUG' at $WORKBENCH_ROOT" >&2
|
|
102
|
-
exit 1
|
|
103
|
-
fi
|
|
104
|
-
if [[ ${#candidate_paths[@]} -gt 1 ]]; then
|
|
105
|
-
echo "Error: multiple worktrees match slug '$SLUG':" >&2
|
|
106
|
-
for p in "${candidate_paths[@]}"; do
|
|
107
|
-
printf ' %s\n' "$(basename "$p")" >&2
|
|
108
|
-
done
|
|
109
|
-
exit 1
|
|
110
|
-
fi
|
|
111
|
-
|
|
112
|
-
WORKTREE_PATH="${candidate_paths[0]}"
|
|
113
77
|
WORKTREE_NAME="$(basename "$WORKTREE_PATH")"
|
|
114
78
|
|
|
115
79
|
# Read preflight[] from config. Each entry: { name, command, when? }.
|
|
@@ -36,7 +36,9 @@ export WORKBENCH_ROOT
|
|
|
36
36
|
REPO="$(_resolve_workbench_repo "${REPO_ARG:-}")"
|
|
37
37
|
|
|
38
38
|
SIBLING_PARENT="$(_read_repos_root)"
|
|
39
|
-
|
|
39
|
+
# Trunk from CONFIG (declared `path` honored) — workbench-trust-fixes, F5.
|
|
40
|
+
CLIENT_ROOT="$(_wt_resolve_trunk_dir "$REPO")"
|
|
41
|
+
CLIENT_ROOT_REAL="$(cd "$CLIENT_ROOT" && pwd -P)"
|
|
40
42
|
|
|
41
43
|
CONFIG_FILE="$WORKBENCH_ROOT/.indusk/worktree-configs/${REPO}.json"
|
|
42
44
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
@@ -54,8 +56,10 @@ refresh_one() {
|
|
|
54
56
|
echo " SKIP: $slug — directory not found"
|
|
55
57
|
return 0
|
|
56
58
|
fi
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
# The trunk is excluded by resolved path, not by name — under a declared
|
|
60
|
+
# `path` its basename need not be the repo name.
|
|
61
|
+
if [[ "$(cd "$worktree_path" && pwd -P)" == "$CLIENT_ROOT_REAL" ]]; then
|
|
62
|
+
return 0 # the trunk checkout, not a worktree
|
|
59
63
|
fi
|
|
60
64
|
# Check this is actually a git worktree (has a .git file pointing into
|
|
61
65
|
# the canonical clone's worktrees/ dir).
|
|
@@ -65,7 +69,7 @@ refresh_one() {
|
|
|
65
69
|
fi
|
|
66
70
|
|
|
67
71
|
echo ""
|
|
68
|
-
echo "Refreshing: $slug"
|
|
72
|
+
echo "Refreshing: $slug ($worktree_path)"
|
|
69
73
|
|
|
70
74
|
local gitdir
|
|
71
75
|
gitdir="$(cd "$worktree_path" && git rev-parse --git-dir)"
|
|
@@ -180,13 +184,17 @@ refresh_one() {
|
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
if [[ "$ARG" == "--all" ]]; then
|
|
183
|
-
echo "Refreshing all worktrees in $WORKBENCH_ROOT (excluding the trunk
|
|
184
|
-
|
|
185
|
-
[[ -
|
|
186
|
-
|
|
187
|
-
done
|
|
187
|
+
echo "Refreshing all worktrees in $WORKBENCH_ROOT and its declared worktrees dirs (excluding the trunk)..."
|
|
188
|
+
while IFS= read -r d; do
|
|
189
|
+
[[ -n "$d" ]] && refresh_one "$d"
|
|
190
|
+
done < <(_wt_list_worktree_dirs)
|
|
188
191
|
else
|
|
189
|
-
|
|
192
|
+
# One resolution surface for a slug — the root and every declared worktrees
|
|
193
|
+
# dir, exact match first, `-<slug>` suffix as fallback, ambiguity refused by
|
|
194
|
+
# name — the resolver behind `wt` (workbench-trust-fixes, F5). A slug that
|
|
195
|
+
# resolves to nothing is an error, not a SKIP that exits 0.
|
|
196
|
+
target="$(_wt_resolve_target "$ARG")" || exit 1
|
|
197
|
+
refresh_one "$target"
|
|
190
198
|
fi
|
|
191
199
|
|
|
192
200
|
echo ""
|
|
@@ -66,11 +66,19 @@ if [[ -z "$SIBLING_PARENT" ]]; then
|
|
|
66
66
|
exit 1
|
|
67
67
|
fi
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
# The trunk checkout from CONFIG — declared `path` honored — through the one
|
|
70
|
+
# resolver `wt` has used since 1.42.0. Building "$SIBLING_PARENT/$REPO" by
|
|
71
|
+
# name failed outright on any repo declared at a path (workbench-trust-fixes, F5).
|
|
72
|
+
CLIENT_ROOT="$(_wt_resolve_trunk_dir "$REPO")"
|
|
73
|
+
if [[ ! -e "$CLIENT_ROOT/.git" ]]; then
|
|
74
|
+
echo "Error: $CLIENT_ROOT is not a git checkout" >&2
|
|
72
75
|
exit 1
|
|
73
76
|
fi
|
|
77
|
+
# A declared worktrees location applies whether or not the TS wrapper passed
|
|
78
|
+
# --worktrees-dir — this script is also run directly.
|
|
79
|
+
if [[ -z "$WORKTREES_DIR" ]]; then
|
|
80
|
+
WORKTREES_DIR="$(_wt_declared_worktrees_dir "$REPO")"
|
|
81
|
+
fi
|
|
74
82
|
|
|
75
83
|
CONFIG_FILE="$WORKBENCH_ROOT/.indusk/worktree-configs/${REPO}.json"
|
|
76
84
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
package/hooks/_hook-paths.js
CHANGED
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
* The 4 hooks (eval-trigger, check-catchup, check-gates, validate-impl-structure)
|
|
5
5
|
* each used to carry their own copy of `findProjectRoot()` that walked up looking
|
|
6
6
|
* for `.indusk/`. In workbench-shaped projects that landed at the workbench root
|
|
7
|
-
* — which
|
|
8
|
-
* path silently bailed. The eval pipeline never
|
|
9
|
-
* 2 months. See
|
|
7
|
+
* — which, before 1.37.0, was not a git repo. Hooks that then tried
|
|
8
|
+
* `git rev-parse` against that path silently bailed. The eval pipeline never
|
|
9
|
+
* fired on numero_workbench for 2 months. See
|
|
10
|
+
* `.indusk/planning/workbench-mode-rail-integrity/`.
|
|
11
|
+
*
|
|
12
|
+
* Since versioned-workbench (1.37.0) the root IS a git repo, and the failure
|
|
13
|
+
* inverted: the walk-up finds the workbench and would attribute a commit to
|
|
14
|
+
* its history of plan documents. `resolveStateAndGitPaths` refuses that
|
|
15
|
+
* (workbench-trust-fixes, F2).
|
|
10
16
|
*
|
|
11
17
|
* The fix: hooks need TWO paths, not one.
|
|
12
18
|
*
|
|
@@ -139,11 +145,11 @@ function findGitPathFromWorkbenchConfig(statePath) {
|
|
|
139
145
|
} catch {
|
|
140
146
|
return null;
|
|
141
147
|
}
|
|
142
|
-
const declared =
|
|
148
|
+
const declared = declaredRepos(config);
|
|
143
149
|
// Ambiguous: more than one repo could hold this commit. Refuse rather than
|
|
144
150
|
// attribute it to whichever happens to be declared first.
|
|
145
151
|
if (declared.length !== 1) return null;
|
|
146
|
-
const candidate = resolve(statePath, declared[0]);
|
|
152
|
+
const candidate = resolve(statePath, declared[0].dir);
|
|
147
153
|
if (!existsSync(candidate)) return null;
|
|
148
154
|
try {
|
|
149
155
|
return realpathSync(candidate);
|
|
@@ -153,16 +159,18 @@ function findGitPathFromWorkbenchConfig(statePath) {
|
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
/**
|
|
156
|
-
* The declared
|
|
162
|
+
* The declared repos, singular reduced into plural, each with the directory
|
|
163
|
+
* its checkout lives in relative to the workbench root.
|
|
157
164
|
*
|
|
158
|
-
* Deliberate port of `readWorkbenchRepos`
|
|
159
|
-
* hooks are plain JS and cannot import the TS
|
|
160
|
-
* Name
|
|
165
|
+
* Deliberate port of `readWorkbenchRepos` + `repoDir` in
|
|
166
|
+
* `src/lib/worktree/repos.ts` — hooks are plain JS and cannot import the TS
|
|
167
|
+
* module. Change both together. Name and dir only: the hook never needs
|
|
168
|
+
* remotes.
|
|
161
169
|
*
|
|
162
170
|
* @param {unknown} config
|
|
163
|
-
* @returns {string[]}
|
|
171
|
+
* @returns {{ name: string, dir: string }[]}
|
|
164
172
|
*/
|
|
165
|
-
function
|
|
173
|
+
function declaredRepos(config) {
|
|
166
174
|
const worktree = config && typeof config === "object" ? config.worktree : null;
|
|
167
175
|
if (!worktree || typeof worktree !== "object") return [];
|
|
168
176
|
const raw = Array.isArray(worktree.repos)
|
|
@@ -171,7 +179,7 @@ function declaredRepoNames(config) {
|
|
|
171
179
|
? [{ name: worktree.wrapped_repo }]
|
|
172
180
|
: [];
|
|
173
181
|
const seen = new Set();
|
|
174
|
-
const
|
|
182
|
+
const repos = [];
|
|
175
183
|
for (const entry of raw) {
|
|
176
184
|
if (!entry || typeof entry !== "object") continue;
|
|
177
185
|
const name = entry.name;
|
|
@@ -184,9 +192,34 @@ function declaredRepoNames(config) {
|
|
|
184
192
|
!name.includes("\\");
|
|
185
193
|
if (!clean || seen.has(name)) continue;
|
|
186
194
|
seen.add(name);
|
|
187
|
-
|
|
195
|
+
// Where the checkout lives: the declared `path`, else the name.
|
|
196
|
+
// Deliberate port of `repoDir` in `src/lib/worktree/repos.ts` — every
|
|
197
|
+
// other surface reads a repo at its declared path, and looking it up by
|
|
198
|
+
// name here returned null (dark) for any repo declared elsewhere (F2).
|
|
199
|
+
repos.push({ name, dir: usableRelPath(entry.path) ?? name });
|
|
200
|
+
}
|
|
201
|
+
return repos;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function declaredRepoNames(config) {
|
|
205
|
+
return declaredRepos(config).map((r) => r.name);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A relative path with no empty, `.` or `..` segment; else null. Port of `isUsableRelPath`. */
|
|
209
|
+
function usableRelPath(value) {
|
|
210
|
+
if (typeof value !== "string") return null;
|
|
211
|
+
const v = value.trim();
|
|
212
|
+
if (v === "" || v.startsWith("/") || v.includes("\\")) return null;
|
|
213
|
+
if (v.split("/").some((s) => s === "" || s === "." || s === "..")) return null;
|
|
214
|
+
return v;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function samePath(a, b) {
|
|
218
|
+
try {
|
|
219
|
+
return realpathSync(a) === realpathSync(b);
|
|
220
|
+
} catch {
|
|
221
|
+
return a === b;
|
|
188
222
|
}
|
|
189
|
-
return names;
|
|
190
223
|
}
|
|
191
224
|
|
|
192
225
|
/**
|
|
@@ -211,7 +244,9 @@ export function declaredReposAt(statePath) {
|
|
|
211
244
|
* Resolve both the InDusk state path and the git path for a given cwd. Either
|
|
212
245
|
* may be null:
|
|
213
246
|
* - `statePath` is null if no `.indusk/` directory exists in any ancestor.
|
|
214
|
-
* - `gitPath` is null if `cwd` is not inside any git repo
|
|
247
|
+
* - `gitPath` is null if `cwd` is not inside any git repo — or if the only
|
|
248
|
+
* repo found is the workbench itself and the declaration cannot name one
|
|
249
|
+
* code repo. Then `refusal` says why, for the caller to surface.
|
|
215
250
|
*
|
|
216
251
|
* The two paths may be the SAME (single-repo mode) or DIFFERENT (workbench mode).
|
|
217
252
|
* Callers must use the right path for the right operation:
|
|
@@ -219,11 +254,37 @@ export function declaredReposAt(statePath) {
|
|
|
219
254
|
* - Git operations (`git rev-parse --short HEAD`, etc.) → use `gitPath`
|
|
220
255
|
*
|
|
221
256
|
* @param {string} cwd
|
|
222
|
-
* @returns {{ statePath: string | null, gitPath: string | null }}
|
|
257
|
+
* @returns {{ statePath: string | null, gitPath: string | null, refusal: string | null, attribution: string | null }}
|
|
223
258
|
*/
|
|
224
259
|
export function resolveStateAndGitPaths(cwd) {
|
|
225
260
|
const statePath = findStatePath(cwd);
|
|
226
261
|
let gitPath = findGitPathFromCwd(cwd);
|
|
262
|
+
let refusal = null;
|
|
263
|
+
let attribution = null;
|
|
264
|
+
// A versioned workbench root IS a git repo, so the walk-up finds it. Its
|
|
265
|
+
// history is plan documents, and it CAN receive commits (a checkoff, a
|
|
266
|
+
// brief) — so when the repo found is the workbench itself, the commit may
|
|
267
|
+
// have gone to it or to the code repo, and the event cannot say which
|
|
268
|
+
// (`event.cwd` is the session cwd, not the subprocess's). With one repo
|
|
269
|
+
// declared, whichever has the NEWER HEAD received the commit; a tie or a
|
|
270
|
+
// missing code repo prefers the code repo, the 1.31.10 behaviour
|
|
271
|
+
// (workbench-trust-fixes F2, refined by falsification A20 — "never the
|
|
272
|
+
// workbench" was the wrong invariant). With several declared, refuse. A
|
|
273
|
+
// flat project declares no repos and keeps gitPath === statePath.
|
|
274
|
+
if (
|
|
275
|
+
gitPath &&
|
|
276
|
+
statePath &&
|
|
277
|
+
declaredReposAt(statePath).length > 0 &&
|
|
278
|
+
samePath(gitPath, statePath)
|
|
279
|
+
) {
|
|
280
|
+
const workbenchRoot = gitPath;
|
|
281
|
+
const codeRepo = findGitPathFromWorkbenchConfig(statePath);
|
|
282
|
+
if (declaredReposAt(statePath).length === 1) {
|
|
283
|
+
({ gitPath, attribution } = attributeRootCommit(workbenchRoot, codeRepo));
|
|
284
|
+
} else {
|
|
285
|
+
gitPath = null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
227
288
|
if (!gitPath) {
|
|
228
289
|
// Workbench-mode fallback (1.31.10). When event.cwd is the workbench
|
|
229
290
|
// root, `git rev-parse` against it fails because the workbench root
|
|
@@ -233,6 +294,57 @@ export function resolveStateAndGitPaths(cwd) {
|
|
|
233
294
|
// from the workbench root (the dominant operating model on Numero
|
|
234
295
|
// and any future FDE engagement).
|
|
235
296
|
gitPath = findGitPathFromWorkbenchConfig(statePath);
|
|
297
|
+
if (!gitPath) {
|
|
298
|
+
const names = declaredReposAt(statePath);
|
|
299
|
+
if (names.length > 1) {
|
|
300
|
+
refusal =
|
|
301
|
+
`eval: a commit made from ${cwd} could belong to any of the ${names.length} repos this workbench declares (${names.join(", ")}), ` +
|
|
302
|
+
"and attributing it to the wrong one is indistinguishable from attributing it right. Refusing to score it. " +
|
|
303
|
+
"Commit from inside the repo the change lives in, or run the evaluator there.";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { statePath, gitPath, refusal, attribution };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Which repository received a commit made from a session sitting at the
|
|
312
|
+
* workbench root, when exactly one code repo is declared: the one whose HEAD
|
|
313
|
+
* is newer. A tie, or a code repo not yet on disk, goes to the code repo — the
|
|
314
|
+
* 1.31.10 behaviour, and the safer error (a plan-document commit scored as
|
|
315
|
+
* code is odd; a code commit never scored is a gap in the rail).
|
|
316
|
+
*
|
|
317
|
+
* @param {string} workbenchRoot
|
|
318
|
+
* @param {string | null} codeRepo
|
|
319
|
+
* @returns {{ gitPath: string, attribution: string }}
|
|
320
|
+
*/
|
|
321
|
+
function attributeRootCommit(workbenchRoot, codeRepo) {
|
|
322
|
+
if (!codeRepo) {
|
|
323
|
+
return {
|
|
324
|
+
gitPath: workbenchRoot,
|
|
325
|
+
attribution: "the workbench (the one declared repo is not on disk)",
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (headCommitTime(workbenchRoot) > headCommitTime(codeRepo)) {
|
|
329
|
+
return {
|
|
330
|
+
gitPath: workbenchRoot,
|
|
331
|
+
attribution: "the workbench (newer HEAD than the declared repo)",
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return { gitPath: codeRepo, attribution: "the declared repo (newer or equal HEAD)" };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** HEAD's committer timestamp (seconds), or -1 when the repo has no commits or is not one. */
|
|
338
|
+
function headCommitTime(dir) {
|
|
339
|
+
try {
|
|
340
|
+
const out = execFileSync("git", ["log", "-1", "--format=%ct"], {
|
|
341
|
+
cwd: dir,
|
|
342
|
+
encoding: "utf-8",
|
|
343
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
344
|
+
});
|
|
345
|
+
const n = Number.parseInt(out.trim(), 10);
|
|
346
|
+
return Number.isFinite(n) ? n : -1;
|
|
347
|
+
} catch {
|
|
348
|
+
return -1;
|
|
236
349
|
}
|
|
237
|
-
return { statePath, gitPath };
|
|
238
350
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one hook-side walk from an impl body to its phases and checkbox items.
|
|
3
|
+
*
|
|
4
|
+
* Deliberate port of the TS phase/item walk in `src/lib/impl-parser.ts` (what
|
|
5
|
+
* `getPhaseCompletion` reads). Hooks are plain JS and cannot import the TS
|
|
6
|
+
* module, so the rule lives in two places — this file and that one — and they
|
|
7
|
+
* must change together.
|
|
8
|
+
*
|
|
9
|
+
* Hook-local (`_`-prefixed): imported by hooks, never registered as one, so it
|
|
10
|
+
* needs no settings entry — but it must exist in `.claude/hooks/` or every
|
|
11
|
+
* importer dies at load. `globSync("*.js")` copies it on init and update.
|
|
12
|
+
*
|
|
13
|
+
* **One definition, shared by `check-gates.js` and `gate-reminder.js`, pinned
|
|
14
|
+
* by A24.** Each carried its own copy of this walk until workbench-trust-fixes'
|
|
15
|
+
* cleanup phase; gate-reminder's was the same walk minus the item text, and it
|
|
16
|
+
* recognized one gate kind fewer. That is the shape `_trajectory-parser.js`
|
|
17
|
+
* was extracted from — two copies that had already diverged without anything
|
|
18
|
+
* failing — and the reason this repo extracts hook-side parsers at two rather
|
|
19
|
+
* than waiting for three.
|
|
20
|
+
*
|
|
21
|
+
* Phases are two sequences ordered by document position (`Test Phase N`,
|
|
22
|
+
* `Build Phase N`; `Phase N` means build phase N). Fenced blocks are content,
|
|
23
|
+
* not structure: a deferral may carry a test body with checkbox- and
|
|
24
|
+
* heading-shaped lines. Items under a `Forward Intelligence` heading are
|
|
25
|
+
* notes, not work, and are excluded.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
FORWARD_INTELLIGENCE_HEADING,
|
|
30
|
+
fencedLineMask,
|
|
31
|
+
gateHeading,
|
|
32
|
+
parsePhaseHeading,
|
|
33
|
+
} from "./_impl-headings.js";
|
|
34
|
+
import { stripFrontmatter } from "./_trajectory-parser.js";
|
|
35
|
+
|
|
36
|
+
const GATE_KINDS = gateHeading("(Verification|OTel|Context|Document)");
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} content — an impl document, with or without frontmatter
|
|
40
|
+
* @returns {{ kind: "test" | "build", number: number, ordinal: number, name: string,
|
|
41
|
+
* items: { checked: boolean, text: string, gate: string }[] }[]}
|
|
42
|
+
*/
|
|
43
|
+
export function parseImplPhases(content) {
|
|
44
|
+
const lines = stripFrontmatter(content).split("\n");
|
|
45
|
+
const fenced = fencedLineMask(lines);
|
|
46
|
+
const phases = [];
|
|
47
|
+
let currentPhase = null;
|
|
48
|
+
let currentGateType = "implementation";
|
|
49
|
+
for (let i = 0; i < lines.length; i++) {
|
|
50
|
+
const line = lines[i];
|
|
51
|
+
if (fenced[i]) continue;
|
|
52
|
+
const phaseMatch = parsePhaseHeading(line);
|
|
53
|
+
if (phaseMatch) {
|
|
54
|
+
if (currentPhase) phases.push(currentPhase);
|
|
55
|
+
currentPhase = {
|
|
56
|
+
number: phaseMatch.number,
|
|
57
|
+
kind: phaseMatch.kind,
|
|
58
|
+
ordinal: phases.length,
|
|
59
|
+
name: phaseMatch.name,
|
|
60
|
+
items: [],
|
|
61
|
+
};
|
|
62
|
+
currentGateType = "implementation";
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// [1] is the phase number, [2] the gate kind — see gateHeading().
|
|
66
|
+
const gateMatch = line.match(GATE_KINDS);
|
|
67
|
+
if (gateMatch) {
|
|
68
|
+
currentGateType = gateMatch[2].toLowerCase();
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (FORWARD_INTELLIGENCE_HEADING.test(line)) {
|
|
72
|
+
currentGateType = "_fi";
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (currentPhase && currentGateType !== "_fi") {
|
|
76
|
+
const itemMatch = line.match(/^-\s+\[([ x])\]\s+(.*)/);
|
|
77
|
+
if (itemMatch) {
|
|
78
|
+
currentPhase.items.push({
|
|
79
|
+
checked: itemMatch[1] === "x",
|
|
80
|
+
text: itemMatch[2].trim(),
|
|
81
|
+
gate: currentGateType,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (currentPhase) phases.push(currentPhase);
|
|
87
|
+
return phases;
|
|
88
|
+
}
|
package/hooks/check-gates.js
CHANGED
|
@@ -12,15 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
14
14
|
import { resolveStateAndGitPaths } from "./_hook-paths.js";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
fencedLineMask,
|
|
18
|
-
gateHeading,
|
|
19
|
-
parsePhaseHeading,
|
|
20
|
-
phaseExists,
|
|
21
|
-
phaseOrdinal,
|
|
22
|
-
phaseSequence,
|
|
23
|
-
} from "./_impl-headings.js";
|
|
15
|
+
import { phaseExists, phaseOrdinal, phaseSequence } from "./_impl-headings.js";
|
|
16
|
+
import { parseImplPhases as parsePhases } from "./_impl-phases.js";
|
|
24
17
|
import { parseTrajectoryFromBody, stripFrontmatter } from "./_trajectory-parser.js";
|
|
25
18
|
|
|
26
19
|
// Read hook input from stdin
|
|
@@ -168,67 +161,7 @@ const WORKFLOW_GATES = Object.fromEntries(
|
|
|
168
161
|
]),
|
|
169
162
|
);
|
|
170
163
|
|
|
171
|
-
//
|
|
172
|
-
function parsePhases(content) {
|
|
173
|
-
// Strip frontmatter
|
|
174
|
-
const fmMatch = content.match(/^---\n[\s\S]*?\n---\n/);
|
|
175
|
-
const body = fmMatch ? content.slice(fmMatch[0].length) : content;
|
|
176
|
-
|
|
177
|
-
const lines = body.split("\n");
|
|
178
|
-
// Fenced blocks are content, not structure — a deferral may carry the
|
|
179
|
-
// deferred test's body, which contains checkbox- and heading-shaped lines.
|
|
180
|
-
const fenced = fencedLineMask(lines);
|
|
181
|
-
const phases = [];
|
|
182
|
-
let currentPhase = null;
|
|
183
|
-
let currentGateType = "implementation";
|
|
184
|
-
|
|
185
|
-
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
186
|
-
const line = lines[lineIndex];
|
|
187
|
-
if (fenced[lineIndex]) continue;
|
|
188
|
-
|
|
189
|
-
const phaseMatch = parsePhaseHeading(line);
|
|
190
|
-
if (phaseMatch) {
|
|
191
|
-
if (currentPhase) phases.push(currentPhase);
|
|
192
|
-
currentPhase = {
|
|
193
|
-
number: phaseMatch.number,
|
|
194
|
-
kind: phaseMatch.kind,
|
|
195
|
-
// Position in the document — the only thing that orders two
|
|
196
|
-
// independently-numbered sequences.
|
|
197
|
-
ordinal: phases.length,
|
|
198
|
-
name: phaseMatch.name,
|
|
199
|
-
items: [],
|
|
200
|
-
};
|
|
201
|
-
currentGateType = "implementation";
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// [1] is the phase number, [2] the gate kind — see gateHeading().
|
|
206
|
-
const gateMatch = line.match(gateHeading("(Verification|OTel|Context|Document)"));
|
|
207
|
-
if (gateMatch) {
|
|
208
|
-
currentGateType = gateMatch[2].toLowerCase();
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Forward intelligence — skip
|
|
213
|
-
if (line.match(FORWARD_INTELLIGENCE_HEADING)) {
|
|
214
|
-
currentGateType = "_fi";
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (currentPhase && currentGateType !== "_fi") {
|
|
219
|
-
const itemMatch = line.match(/^-\s+\[([ x])\]\s+(.*)/);
|
|
220
|
-
if (itemMatch) {
|
|
221
|
-
currentPhase.items.push({
|
|
222
|
-
checked: itemMatch[1] === "x",
|
|
223
|
-
text: itemMatch[2].trim(),
|
|
224
|
-
gate: currentGateType,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (currentPhase) phases.push(currentPhase);
|
|
230
|
-
return phases;
|
|
231
|
-
}
|
|
164
|
+
// The phase/item walk is shared with gate-reminder.js — one definition, pinned by A24.
|
|
232
165
|
|
|
233
166
|
/** Name a phase unambiguously: the two sequences share their digits. */
|
|
234
167
|
function phaseLabel(kind, number) {
|
package/hooks/eval-trigger.js
CHANGED
|
@@ -91,7 +91,12 @@ if (cliSource !== null || drainPending) {
|
|
|
91
91
|
// early syslog calls used raw `cwd` and silently created stray `.indusk/`
|
|
92
92
|
// directories inside wrapped repos — exactly the "no lingering app-level
|
|
93
93
|
// state" pattern this plan is fixing.
|
|
94
|
-
const {
|
|
94
|
+
const {
|
|
95
|
+
statePath: resolvedStatePath,
|
|
96
|
+
gitPath,
|
|
97
|
+
refusal: attributionRefusal,
|
|
98
|
+
attribution,
|
|
99
|
+
} = resolveStateAndGitPaths(cwd);
|
|
95
100
|
const statePath = resolvedStatePath ?? cwd;
|
|
96
101
|
|
|
97
102
|
if (cliSource !== null || drainPending) {
|
|
@@ -156,7 +161,7 @@ const evalConfig = readEvalConfig(statePath);
|
|
|
156
161
|
|
|
157
162
|
syslog(
|
|
158
163
|
statePath,
|
|
159
|
-
`statePath: ${statePath}, gitPath: ${gitPath ?? "(none)"}, eval.enabled: ${evalConfig.enabled}`,
|
|
164
|
+
`statePath: ${statePath}, gitPath: ${gitPath ?? "(none)"}, eval.enabled: ${evalConfig.enabled}${attribution ? ` — attributed to ${attribution}` : ""}`,
|
|
160
165
|
);
|
|
161
166
|
|
|
162
167
|
// Check if eval is disabled
|
|
@@ -194,9 +199,10 @@ if (drainPending) {
|
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
// Get the current commit ID. Runs against gitPath, not statePath — in
|
|
197
|
-
// workbench mode the two differ (statePath = workbench root,
|
|
198
|
-
// gitPath =
|
|
199
|
-
// and bailed on every commit in
|
|
202
|
+
// workbench mode the two differ (statePath = workbench root, whose own git
|
|
203
|
+
// history is plan documents; gitPath = the declared code repo or a worktree).
|
|
204
|
+
// Pre-1.31.7 ran against statePath and bailed on every commit in
|
|
205
|
+
// workbench-shaped projects; post-1.37.0 it would have scored the workbench.
|
|
200
206
|
let changeId = changeIdArg ?? undefined;
|
|
201
207
|
if (!changeId && gitPath) {
|
|
202
208
|
try {
|
|
@@ -212,6 +218,26 @@ if (!changeId && gitPath) {
|
|
|
212
218
|
}
|
|
213
219
|
}
|
|
214
220
|
if (!changeId) {
|
|
221
|
+
if (!gitPath && attributionRefusal) {
|
|
222
|
+
// Refuse VISIBLY. system.log is where the eval rail talks to itself; a
|
|
223
|
+
// commit the evaluator silently declined to score looks, to the
|
|
224
|
+
// session, exactly like one it scored. stdout JSON is the one channel a
|
|
225
|
+
// PostToolUse hook has to the model at exit 0 (workbench-trust-fixes,
|
|
226
|
+
// F2 / A8) — and only there: in CLI and drain modes stdout is a
|
|
227
|
+
// terminal or a parent process, not the model (falsification A21).
|
|
228
|
+
syslog(statePath, `refusing to attribute — ${attributionRefusal}`);
|
|
229
|
+
if (cliSource === null && !drainPending) {
|
|
230
|
+
console.info(
|
|
231
|
+
JSON.stringify({
|
|
232
|
+
hookSpecificOutput: {
|
|
233
|
+
hookEventName: "PostToolUse",
|
|
234
|
+
additionalContext: attributionRefusal,
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
process.exit(0);
|
|
240
|
+
}
|
|
215
241
|
syslog(
|
|
216
242
|
statePath,
|
|
217
243
|
gitPath
|