@nodii/dev 0.6.29 → 0.6.31
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/README.md +52 -1
- package/dist/cli.js +63 -9
- package/dist/core.js +37 -14
- package/dist/devops-openapi.json +1 -1
- package/dist/doctor.js +18 -1
- package/dist/freeze.js +328 -0
- package/dist/gather.js +241 -0
- package/dist/harness.js +2 -0
- package/dist/hooks.js +16 -42
- package/dist/lanes.js +170 -9
- package/dist/managed-hooks.js +6 -1
- package/dist/nodii-dev.js +12 -2
- package/dist/nudges.js +441 -0
- package/dist/setup.js +23 -6
- package/dist/starting.js +52 -23
- package/dist/upload.js +11 -1
- package/package.json +1 -1
package/dist/hooks.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { CliError, devRequest, readConfig } from "./core.js";
|
|
2
2
|
import { FOUNDATIONS_ID, figmaLockCheck, outOfScopeText } from "./figma.js";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
|
-
import { readFile
|
|
5
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
6
5
|
import { promisify } from "node:util";
|
|
7
6
|
import { safetySettingsSnippet } from "./safety.js";
|
|
7
|
+
import { regroundCurrentLane } from "./freeze.js";
|
|
8
8
|
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
|
|
@@ -228,46 +228,20 @@ export async function heartbeatCurrentLane(request = devRequest) {
|
|
|
228
228
|
/** SessionStart compact context, plain text for both Claude and Codex. */
|
|
229
229
|
export async function sessionStartContext(payload, { request = devRequest, config = readConfig } = {}) {
|
|
230
230
|
if (payload?.source !== "compact") return null;
|
|
231
|
-
let
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (lane) {
|
|
246
|
-
const freeze = lane.freeze ?? {};
|
|
247
|
-
return [
|
|
248
|
-
`Freeze re-grounding for ${lane.key}:`,
|
|
249
|
-
`title: ${lane.title ?? "(untitled)"}`,
|
|
250
|
-
`status: ${lane.status}`,
|
|
251
|
-
`boundaries: ${JSON.stringify(freeze.boundaries ?? {})}`,
|
|
252
|
-
`rules: ${JSON.stringify(freeze.rules ?? freeze.ruleRefs ?? [])}`,
|
|
253
|
-
].join("\n");
|
|
254
|
-
}
|
|
255
|
-
const lanes = (await request("GET", "/api/v1/lanes?limit=200")).data ?? [];
|
|
256
|
-
for (const item of lanes) {
|
|
257
|
-
if (item.agentId !== current.agentId || item.branch !== branch || ["on_dev", "stopped"].includes(item.status)) continue;
|
|
258
|
-
const worktree = await realpath(item.worktree ?? root).catch(() => resolve(item.worktree ?? root));
|
|
259
|
-
if (worktree === root) { lane = item; break; }
|
|
260
|
-
}
|
|
261
|
-
} catch {}
|
|
262
|
-
if (!lane) return "Freeze re-grounding: no active Dev lane was available for this branch; re-read the lane brief before continuing.";
|
|
263
|
-
const freeze = lane.freeze ?? {};
|
|
264
|
-
return [
|
|
265
|
-
`Freeze re-grounding for ${lane.key}:`,
|
|
266
|
-
`title: ${lane.title ?? "(untitled)"}`,
|
|
267
|
-
`status: ${lane.status}`,
|
|
268
|
-
`boundaries: ${JSON.stringify(freeze.boundaries ?? {})}`,
|
|
269
|
-
`rules: ${JSON.stringify(freeze.rules ?? freeze.ruleRefs ?? [])}`,
|
|
270
|
-
].join("\n");
|
|
231
|
+
let context = "";
|
|
232
|
+
const result = await regroundCurrentLane(
|
|
233
|
+
{
|
|
234
|
+
cwd: payload.cwd ?? process.cwd(),
|
|
235
|
+
force: true,
|
|
236
|
+
write: (text) => {
|
|
237
|
+
context += text;
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{ request, config },
|
|
241
|
+
);
|
|
242
|
+
return result.shown
|
|
243
|
+
? context.trimEnd()
|
|
244
|
+
: "Freeze re-grounding: no active Dev lane with initialized Freeze was available for this worktree; run `nodii-dev lane freeze show <key>` before continuing.";
|
|
271
245
|
}
|
|
272
246
|
|
|
273
247
|
export function workflowSuggestion(payload) {
|
package/dist/lanes.js
CHANGED
|
@@ -4,9 +4,12 @@ import { promisify } from "node:util";
|
|
|
4
4
|
import { CliError, encode, devRequest, query, readConfig, requireFlag } from "./core.js";
|
|
5
5
|
import { launchRecovery } from "./runtime-launcher.js";
|
|
6
6
|
import { recordReviewReceipt, reviewCommitContext } from "./safety.js";
|
|
7
|
+
import { GATHER_TIMEOUT_MS, gatherText, pickDocuments } from "./gather.js";
|
|
8
|
+
import { storeGatherFreeze } from "./freeze.js";
|
|
7
9
|
|
|
8
10
|
const execFileAsync = promisify(execFile);
|
|
9
11
|
const via = { via: "nodii-dev" };
|
|
12
|
+
const GATHER_ACTIVE = new Set(["queued", "building"]);
|
|
10
13
|
// heartbeatLane's note limit.
|
|
11
14
|
const BLOCK_NOTE_MAX = 500;
|
|
12
15
|
|
|
@@ -118,7 +121,34 @@ export async function branchCommits(lane) {
|
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
|
|
124
|
+
async function currentGatherLane(request, config, gitContext) {
|
|
125
|
+
const c = await config();
|
|
126
|
+
const context = await gitContext();
|
|
127
|
+
const lanes = [];
|
|
128
|
+
let cursor;
|
|
129
|
+
do {
|
|
130
|
+
const page = await request("GET", `/api/v1/lanes${query({ agent: c.agentId, cursor, limit: 200 })}`);
|
|
131
|
+
lanes.push(...(page.data ?? []));
|
|
132
|
+
cursor = page.nextCursor ?? undefined;
|
|
133
|
+
} while (cursor);
|
|
134
|
+
const active = lanes.filter((lane) => GATHER_ACTIVE.has(lane.status));
|
|
135
|
+
const repositories = (lane) => lane.repositories?.length
|
|
136
|
+
? lane.repositories
|
|
137
|
+
: lane.freeze?.boundaries?.repos?.length
|
|
138
|
+
? lane.freeze.boundaries.repos
|
|
139
|
+
: [{ repo: lane.repos?.[0], branch: lane.branch, worktree: lane.worktree }];
|
|
140
|
+
const exact = active.filter((lane) => repositories(lane).some((repository) => repository.worktree === context.worktree));
|
|
141
|
+
const branch = active.filter((lane) => repositories(lane).some((repository) =>
|
|
142
|
+
repository.branch === context.branch && repoName(repository.repo ?? "") === context.repo));
|
|
143
|
+
const matches = exact.length ? exact : branch;
|
|
144
|
+
if (matches.length !== 1)
|
|
145
|
+
throw new CliError("CURRENT_LANE_AMBIGUOUS", matches.length
|
|
146
|
+
? `This worktree matches ${matches.length} active lanes (${matches.map((lane) => lane.key).join(", ")}); pass one key explicitly.`
|
|
147
|
+
: "No active lane matches this agent and worktree; pass the lane key explicitly.", 2);
|
|
148
|
+
return matches[0].key;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function laneCommand(action, args, flags, { request = devRequest, config = readConfig, gitContext = inferGitContext, commitsOf = branchCommits, read = (path) => readFile(path, "utf8"), reviewContext = reviewCommitContext, reviewReceipt = recordReviewReceipt, picker = pickDocuments, changedPaths = laneChangedPaths, gatherTimeoutMs = GATHER_TIMEOUT_MS, gatherSignal, storeFreeze = storeGatherFreeze, launch = launchRecovery } = {}) {
|
|
122
152
|
if (action === "list") {
|
|
123
153
|
const c = flags.mine ? await config() : null;
|
|
124
154
|
const payload = await request("GET", `/api/v1/lanes${query({ kind: flags.kind, status: flags.status, machine: flags.machine, agent: flags.mine ? c.agentId : flags.agent, "waiting-on": flags["waiting-on"], stuck: flags.stuck, cursor: flags.cursor, limit: flags.limit })}`);
|
|
@@ -142,12 +172,138 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
|
|
|
142
172
|
const payload = await request("POST", `/api/v1/lanes/${encode(key)}/start`, via);
|
|
143
173
|
return { payload, human: (p) => `Started ${laneLine(p.data)}` };
|
|
144
174
|
}
|
|
145
|
-
const ref = need(args[0], `lane ${action} needs <key>`);
|
|
175
|
+
const ref = need(args[0] ?? (action === "gather" ? await currentGatherLane(request, config, gitContext) : undefined), `lane ${action} needs <key>`);
|
|
146
176
|
const id = ref;
|
|
147
177
|
if (action === "show") {
|
|
148
178
|
const payload = await request("GET", `/api/v1/lanes/${encode(id)}`);
|
|
149
179
|
return { payload, human: (p) => detailText(p.data) };
|
|
150
180
|
}
|
|
181
|
+
if (action === "gather") {
|
|
182
|
+
const controller = gatherSignal ? null : new AbortController();
|
|
183
|
+
const deadline = controller ? setTimeout(() => controller.abort(), gatherTimeoutMs) : null;
|
|
184
|
+
const signal = gatherSignal ?? controller.signal;
|
|
185
|
+
try {
|
|
186
|
+
const lane = (await request("GET", `/api/v1/lanes/${encode(id)}`, undefined, { signal })).data;
|
|
187
|
+
if (flags.objective !== undefined && (typeof flags.objective !== "string" || !flags.objective.trim()))
|
|
188
|
+
throw new CliError("USAGE", "lane gather --objective needs non-empty text", 2);
|
|
189
|
+
if (flags["done-check"] !== undefined && (typeof flags["done-check"] !== "string" || !flags["done-check"].trim()))
|
|
190
|
+
throw new CliError("USAGE", "lane gather --done-check needs non-empty text", 2);
|
|
191
|
+
const durableObjective = [lane.objective, lane.freeze?.objective?.ask].find((x) => typeof x === "string" && x.trim());
|
|
192
|
+
const explicitObjective = typeof flags.objective === "string" ? flags.objective.trim() : undefined;
|
|
193
|
+
if (explicitObjective && durableObjective && explicitObjective !== durableObjective.trim())
|
|
194
|
+
throw new CliError("GATHER_OBJECTIVE_CONFLICT", `${ref} already has a different objective. Use lane amend; Gather cannot rewrite the durable contract.`, 1);
|
|
195
|
+
const issueObjective = typeof lane.issue?.bodyMd === "string" && lane.issue.bodyMd.trim() ? lane.issue.bodyMd : undefined;
|
|
196
|
+
const objective = durableObjective ?? explicitObjective ?? issueObjective;
|
|
197
|
+
if (!objective) throw new CliError("GATHER_OBJECTIVE_MISSING", `${ref} has no recorded objective. Amend the lane or pass --objective; the CLI does not invent one.`, 1);
|
|
198
|
+
if (objective.length > 20_000) throw new CliError("USAGE", "The recorded Gather objective exceeds Dev's 20,000-character limit.", 2);
|
|
199
|
+
const durableDoneCheck = [lane.doneCheck, lane.freeze?.objective?.doneCheck].find((x) => typeof x === "string" && x.trim());
|
|
200
|
+
const explicitDoneCheck = typeof flags["done-check"] === "string" ? flags["done-check"].trim() : undefined;
|
|
201
|
+
if (explicitDoneCheck && durableDoneCheck && explicitDoneCheck !== durableDoneCheck.trim())
|
|
202
|
+
throw new CliError("GATHER_DONE_CHECK_CONFLICT", `${ref} already has a different done check. Use lane amend; Gather cannot rewrite the durable contract.`, 1);
|
|
203
|
+
const doneCheck = durableDoneCheck ?? explicitDoneCheck;
|
|
204
|
+
if (!doneCheck) throw new CliError("GATHER_DONE_CHECK_MISSING", `${ref} has no recorded done check. Pass --done-check before Gather records anything.`, 1);
|
|
205
|
+
if (doneCheck.length > 10_000) throw new CliError("USAGE", "The Gather done check exceeds Dev's 10,000-character limit.", 2);
|
|
206
|
+
const legacyRepository = {
|
|
207
|
+
repo: lane.repos?.[0],
|
|
208
|
+
branch: lane.branch,
|
|
209
|
+
worktree: lane.worktree,
|
|
210
|
+
ownedPaths: lane.ownedPaths ?? [],
|
|
211
|
+
};
|
|
212
|
+
const sourceRepositories = lane.repositories?.length
|
|
213
|
+
? lane.repositories
|
|
214
|
+
: lane.freeze?.boundaries?.repos?.length
|
|
215
|
+
? lane.freeze.boundaries.repos
|
|
216
|
+
: [legacyRepository];
|
|
217
|
+
const repositories = sourceRepositories.map((repository) => Object.fromEntries(
|
|
218
|
+
["repo", "branch", "worktree", "ownedPaths"]
|
|
219
|
+
.map((key) => [key, repository[key]])
|
|
220
|
+
.filter(([, value]) => value !== undefined),
|
|
221
|
+
));
|
|
222
|
+
if (!repositories.length || repositories.some((repository) => !repository.repo || !repository.branch || !repository.worktree))
|
|
223
|
+
throw new CliError("GATHER_BOUNDARIES_MISSING", `${ref} has an incomplete repository boundary. Repair its repo, branch, and worktree before Gather records anything.`, 1);
|
|
224
|
+
const limit = flags.limit === undefined ? undefined : Number(flags.limit);
|
|
225
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 500))
|
|
226
|
+
throw new CliError("USAGE", "lane gather --limit needs a whole number from 1 to 500", 2);
|
|
227
|
+
const sectionLimit = flags["section-limit-per-document"] === undefined ? undefined : Number(flags["section-limit-per-document"]);
|
|
228
|
+
if (sectionLimit !== undefined && (!Number.isInteger(sectionLimit) || sectionLimit < 1 || sectionLimit > 20))
|
|
229
|
+
throw new CliError("USAGE", "lane gather --section-limit-per-document needs a whole number from 1 to 20", 2);
|
|
230
|
+
const recordedPaths = [
|
|
231
|
+
...(lane.repositories ?? []).flatMap((repository) => repository.ownedPaths ?? []),
|
|
232
|
+
...(lane.freeze?.boundaries?.repos ?? []).flatMap((repository) => repository.ownedPaths ?? []),
|
|
233
|
+
...(lane.ownedPaths ?? []),
|
|
234
|
+
];
|
|
235
|
+
const driftPaths = await changedPaths(lane, { signal });
|
|
236
|
+
if (driftPaths === null)
|
|
237
|
+
throw new CliError("GATHER_PATHS_UNAVAILABLE", "Gather could not read every recorded worktree. Repair the lane boundary or make its repositories available before continuing.", 1);
|
|
238
|
+
const paths = flags.paths ? list(flags.paths) : [...new Set([...recordedPaths, ...driftPaths])];
|
|
239
|
+
if (paths.length > 5000) throw new CliError("GATHER_SCOPE_TOO_LARGE", `Gather found ${paths.length} paths; Dev accepts at most 5,000. Narrow the lane's recorded scope before continuing.`, 1);
|
|
240
|
+
const repoRefs = [
|
|
241
|
+
...(lane.repositories ?? []).map((repository) => repository.repo),
|
|
242
|
+
...(lane.freeze?.boundaries?.repos ?? []).map((repository) => repository.repo),
|
|
243
|
+
...(Array.isArray(lane.repos) ? lane.repos.map((repo) => typeof repo === "string" ? repo : repo.repo) : []),
|
|
244
|
+
];
|
|
245
|
+
const repos = [...new Set(repoRefs.filter(Boolean).map(repoName))];
|
|
246
|
+
const components = flags.components ? list(flags.components) : [
|
|
247
|
+
...(lane.freeze?.boundaries?.figma ?? []).flatMap((file) => file.componentSetKeys ?? []),
|
|
248
|
+
...(lane.launch?.figmaScopes ?? []).flatMap((file) => file.componentSets ?? []),
|
|
249
|
+
...(lane.components ?? []),
|
|
250
|
+
];
|
|
251
|
+
const shortlistBody = {
|
|
252
|
+
objective,
|
|
253
|
+
paths,
|
|
254
|
+
repos,
|
|
255
|
+
components,
|
|
256
|
+
...(limit !== undefined ? { limit } : {}),
|
|
257
|
+
};
|
|
258
|
+
const shortlist = await request("POST", "/api/v1/rules/shortlist", shortlistBody, { signal });
|
|
259
|
+
const triggers = shortlist.data?.triggers ?? [];
|
|
260
|
+
const picked = await picker({
|
|
261
|
+
objective,
|
|
262
|
+
files: paths,
|
|
263
|
+
triggers: triggers.map((trigger) => ({
|
|
264
|
+
code: trigger.code,
|
|
265
|
+
trigger: trigger.sentence,
|
|
266
|
+
...(trigger.forcedBy ? { forcedBy: trigger.forcedBy } : {}),
|
|
267
|
+
})),
|
|
268
|
+
}, { signal });
|
|
269
|
+
const payload = await request("POST", `/api/v1/lanes/${encode(id)}/gather`, {
|
|
270
|
+
objective,
|
|
271
|
+
documentCodes: picked.documents,
|
|
272
|
+
paths,
|
|
273
|
+
components,
|
|
274
|
+
mode: flags.delta ? "delta" : "full",
|
|
275
|
+
...(sectionLimit !== undefined ? { sectionLimitPerDocument: sectionLimit } : {}),
|
|
276
|
+
}, { signal });
|
|
277
|
+
const figma = lane.freeze?.boundaries?.figma ?? (lane.launch?.figmaScopes ?? []).map((file) => ({
|
|
278
|
+
fileKey: file.fileKey,
|
|
279
|
+
...(file.componentSets?.length ? { componentSetKeys: file.componentSets } : {}),
|
|
280
|
+
}));
|
|
281
|
+
const issue = lane.issue ? { key: lane.issue.key, title: lane.issue.title } : null;
|
|
282
|
+
const laneNotes = [
|
|
283
|
+
lane.launch?.notes,
|
|
284
|
+
lane.launch?.narrowingNote && `Narrowing: ${lane.launch.narrowingNote}`,
|
|
285
|
+
lane.launch?.narrowingRemainder && `Narrowing remainder: ${lane.launch.narrowingRemainder}`,
|
|
286
|
+
].filter(Boolean);
|
|
287
|
+
const freezeRequest = (method, path, body, options = {}) => request(method, path, body, { ...options, signal });
|
|
288
|
+
await storeFreeze(id, {
|
|
289
|
+
gather: payload.data,
|
|
290
|
+
boundaries: { repos: repositories, ...(figma.length ? { figma } : {}) },
|
|
291
|
+
issue,
|
|
292
|
+
laneNotes,
|
|
293
|
+
doneCheck,
|
|
294
|
+
}, { request: freezeRequest });
|
|
295
|
+
const reasons = Object.fromEntries(triggers.map((trigger) => [
|
|
296
|
+
trigger.code,
|
|
297
|
+
Array.isArray(trigger.forcedBy) && trigger.forcedBy.length ? `forced by ${trigger.forcedBy.join(", ")}` : trigger.sentence,
|
|
298
|
+
]));
|
|
299
|
+
return { payload, human: (p) => gatherText(p, objective, reasons) };
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (signal.aborted) throw new CliError("GATHER_TIMEOUT", `Gather exceeded its ${Math.round(gatherTimeoutMs / 1000)}-second deadline; nothing was retried.`, 1);
|
|
302
|
+
throw error;
|
|
303
|
+
} finally {
|
|
304
|
+
if (deadline) clearTimeout(deadline);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
151
307
|
if (action === "step") {
|
|
152
308
|
const selected = ["start", "done", "block"].filter((x) => flags[x]);
|
|
153
309
|
if (selected.length !== 1) throw new CliError("USAGE", "lane step needs exactly one of --start, --done, or --block.", 2);
|
|
@@ -303,21 +459,26 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
|
|
|
303
459
|
/**
|
|
304
460
|
* The files a lane touches, for path-tagged rules (M05-32): its branch and
|
|
305
461
|
* working-tree changes against origin/main, plus untracked files, read from
|
|
306
|
-
* its recorded worktree. Null when
|
|
307
|
-
*
|
|
462
|
+
* its recorded worktree. Null when any worktree cannot be read here; Gather
|
|
463
|
+
* refuses that unknown scope rather than silently omit path-triggered rules.
|
|
308
464
|
*/
|
|
309
|
-
export async function laneChangedPaths(lane) {
|
|
465
|
+
export async function laneChangedPaths(lane, { signal, exec = execFileAsync } = {}) {
|
|
310
466
|
try {
|
|
311
|
-
const repositories = lane.repositories?.length
|
|
467
|
+
const repositories = lane.repositories?.length
|
|
468
|
+
? lane.repositories
|
|
469
|
+
: lane.freeze?.boundaries?.repos?.length
|
|
470
|
+
? lane.freeze.boundaries.repos
|
|
471
|
+
: [{ repo: lane.repos?.[0], worktree: lane.worktree, baseRef: "origin/main" }];
|
|
312
472
|
const changed = await Promise.all(repositories.map(async (repository) => {
|
|
313
|
-
const run = async (...args) => (await
|
|
473
|
+
const run = async (...args) => (await exec("git", ["-C", repository.worktree, ...args], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, signal })).stdout;
|
|
314
474
|
const base = (await run("merge-base", repository.baseRef ?? "origin/main", "HEAD")).trim();
|
|
315
475
|
const tracked = (await run("diff", "--name-only", base)).split("\n");
|
|
316
476
|
const untracked = (await run("ls-files", "--others", "--exclude-standard")).split("\n");
|
|
317
477
|
return [...tracked, ...untracked].map((path) => path.trim()).filter(Boolean);
|
|
318
478
|
}));
|
|
319
|
-
return [...new Set(changed.flat())]
|
|
320
|
-
} catch {
|
|
479
|
+
return [...new Set(changed.flat())];
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (signal?.aborted) throw error;
|
|
321
482
|
return null;
|
|
322
483
|
}
|
|
323
484
|
}
|
package/dist/managed-hooks.js
CHANGED
|
@@ -135,7 +135,12 @@ export async function dispatchManagedHook(event, payloadText, deps = {}) {
|
|
|
135
135
|
}
|
|
136
136
|
const structured = messages.map((message) => message.hookSpecificOutput?.additionalContext).filter(Boolean);
|
|
137
137
|
const plain = successful.map((result) => responseOf(result.stdout) ? "" : result.stdout.trim()).filter(Boolean);
|
|
138
|
-
|
|
138
|
+
// A Stop nudge asks the session to continue once; any other Stop output is dropped.
|
|
139
|
+
if (event === "Stop") {
|
|
140
|
+
const block = messages.find((message) => message.decision === "block" && typeof message.reason === "string");
|
|
141
|
+
return block ? `${JSON.stringify({ decision: "block", reason: block.reason })}\n` : "{}\n";
|
|
142
|
+
}
|
|
143
|
+
if (event === "SubagentStop") return "{}\n";
|
|
139
144
|
return [...structured, ...plain].join("\n") + ([...structured, ...plain].length ? "\n" : "");
|
|
140
145
|
} catch {
|
|
141
146
|
return emptyResponse(event);
|
package/dist/nodii-dev.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { fail, main } from "./cli.js";
|
|
3
|
+
import { regroundCurrentLane, shouldReground } from "./freeze.js";
|
|
3
4
|
|
|
4
5
|
// Hooks run in the agent's environment, which may be incomplete or may not be
|
|
5
6
|
// a git worktree. An unavailable hook must never turn into a refused tool call.
|
|
6
7
|
// Intentional policy denials are handled inside the hook command before they
|
|
7
8
|
// reach this boundary.
|
|
8
|
-
|
|
9
|
+
const argv = process.argv.slice(2);
|
|
10
|
+
|
|
11
|
+
async function run() {
|
|
12
|
+
// AGENT-HARNESS-19: this is the executable boundary, so every ordinary CLI
|
|
13
|
+
// invocation re-grounds without adding hidden requests to library callers.
|
|
14
|
+
if (shouldReground(argv)) await regroundCurrentLane();
|
|
15
|
+
await main(argv);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
run().catch((error) => {
|
|
9
19
|
const denial = String(error?.code ?? "").startsWith("SAFETY_") ||
|
|
10
20
|
["FIGMA_FILE_KEY_MISSING", "FIGMA_LOCK_DENIED", "WRITE_OUT_OF_SCOPE"].includes(error?.code);
|
|
11
|
-
if (
|
|
21
|
+
if (argv[0] === "hook" && !denial) {
|
|
12
22
|
process.exitCode = 0;
|
|
13
23
|
return;
|
|
14
24
|
}
|