@lovrozagar/grunt 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/grunt.md +2 -24
- package/.agents/agents/orchestrator.md +5 -50
- package/.agents/skills/ask/SKILL.md +5 -12
- package/.agents/skills/auto/SKILL.md +7 -12
- package/.agents/skills/browser/SKILL.md +4 -8
- package/.agents/skills/clasp/SKILL.md +11 -0
- package/.agents/skills/commit/SKILL.md +3 -1
- package/.agents/skills/commit-push/SKILL.md +2 -0
- package/.agents/skills/commit-push-deploy/SKILL.md +2 -0
- package/.agents/skills/commit-push-release/SKILL.md +2 -0
- package/.agents/skills/explain/SKILL.md +7 -13
- package/.agents/skills/google-workspace/SKILL.md +13 -0
- package/.agents/skills/handoff/SKILL.md +7 -11
- package/.agents/skills/implement-plan/SKILL.md +5 -109
- package/.agents/skills/listen/SKILL.md +15 -0
- package/.agents/skills/pickup/SKILL.md +6 -12
- package/.agents/skills/speak/SKILL.md +13 -0
- package/.agents/skills/tmp/SKILL.md +6 -10
- package/.agents/skills/write-plan/SKILL.md +5 -77
- package/.claude/agents/grunt.md +2 -26
- package/.claude/agents/orchestrator.md +12 -50
- package/.claude/skills/ask/SKILL.md +5 -12
- package/.claude/skills/auto/SKILL.md +7 -12
- package/.claude/skills/browser/SKILL.md +4 -8
- package/.claude/skills/clasp/SKILL.md +11 -0
- package/.claude/skills/commit/SKILL.md +3 -1
- package/.claude/skills/commit-push/SKILL.md +2 -0
- package/.claude/skills/commit-push-deploy/SKILL.md +2 -0
- package/.claude/skills/commit-push-release/SKILL.md +2 -0
- package/.claude/skills/explain/SKILL.md +7 -13
- package/.claude/skills/google-workspace/SKILL.md +13 -0
- package/.claude/skills/handoff/SKILL.md +7 -11
- package/.claude/skills/implement-plan/SKILL.md +5 -109
- package/.claude/skills/listen/SKILL.md +15 -0
- package/.claude/skills/pickup/SKILL.md +6 -12
- package/.claude/skills/speak/SKILL.md +13 -0
- package/.claude/skills/tmp/SKILL.md +6 -10
- package/.claude/skills/write-plan/SKILL.md +5 -77
- package/.codex/agents/grunt.toml +2 -26
- package/.codex/agents/orchestrator.toml +4 -50
- package/.grok/agents/grunt.md +2 -24
- package/.grok/agents/orchestrator.md +4 -48
- package/.grok/global-settings.toml +1 -3
- package/.grok/hooks/orchestrate-parent.js +190 -358
- package/.grok/skills/ask/SKILL.md +5 -12
- package/.grok/skills/auto/SKILL.md +7 -12
- package/.grok/skills/browser/SKILL.md +4 -8
- package/.grok/skills/clasp/SKILL.md +11 -0
- package/.grok/skills/commit/SKILL.md +3 -1
- package/.grok/skills/commit-push/SKILL.md +2 -0
- package/.grok/skills/commit-push-deploy/SKILL.md +2 -0
- package/.grok/skills/commit-push-release/SKILL.md +2 -0
- package/.grok/skills/explain/SKILL.md +7 -13
- package/.grok/skills/google-workspace/SKILL.md +13 -0
- package/.grok/skills/handoff/SKILL.md +7 -11
- package/.grok/skills/implement-plan/SKILL.md +5 -109
- package/.grok/skills/listen/SKILL.md +15 -0
- package/.grok/skills/pickup/SKILL.md +6 -12
- package/.grok/skills/speak/SKILL.md +13 -0
- package/.grok/skills/tmp/SKILL.md +6 -10
- package/.grok/skills/write-plan/SKILL.md +5 -77
- package/.rulesync/hooks.jsonc +1 -1
- package/.rulesync/mcp-policy.jsonc +0 -2
- package/.rulesync/reference/INDEX.md +30 -20
- package/.rulesync/reference/browser.md +13 -6
- package/.rulesync/reference/cascade.md +6 -86
- package/.rulesync/reference/clasp.md +21 -0
- package/.rulesync/reference/google-workspace.md +79 -0
- package/.rulesync/reference/hooks.md +7 -11
- package/.rulesync/reference/implementation-format.md +113 -0
- package/.rulesync/reference/law.md +5 -3
- package/.rulesync/reference/listen.md +80 -0
- package/.rulesync/reference/map.md +39 -36
- package/.rulesync/reference/output.md +3 -51
- package/.rulesync/reference/plan-format.md +4 -2
- package/.rulesync/reference/refs-map.md +12 -5
- package/.rulesync/reference/rtk.md +3 -2
- package/.rulesync/reference/scope.md +37 -0
- package/.rulesync/reference/setup.md +27 -0
- package/.rulesync/reference/skills-map.md +13 -12
- package/.rulesync/reference/speak.md +59 -0
- package/.rulesync/rules/CLAUDE.md +3 -49
- package/.rulesync/rules/overview.md +9 -47
- package/.rulesync/skills/ask/SKILL.md +5 -12
- package/.rulesync/skills/auto/SKILL.md +5 -12
- package/.rulesync/skills/browser/SKILL.md +4 -8
- package/.rulesync/skills/clasp/SKILL.md +9 -0
- package/.rulesync/skills/commit/SKILL.md +3 -1
- package/.rulesync/skills/commit-push/SKILL.md +2 -0
- package/.rulesync/skills/commit-push-deploy/SKILL.md +2 -0
- package/.rulesync/skills/commit-push-release/SKILL.md +2 -0
- package/.rulesync/skills/explain/SKILL.md +7 -14
- package/.rulesync/skills/google-workspace/SKILL.md +11 -0
- package/.rulesync/skills/handoff/SKILL.md +7 -11
- package/.rulesync/skills/implement-plan/SKILL.md +5 -119
- package/.rulesync/skills/listen/SKILL.md +13 -0
- package/.rulesync/skills/pickup/SKILL.md +6 -12
- package/.rulesync/skills/speak/SKILL.md +13 -0
- package/.rulesync/skills/tmp/SKILL.md +6 -10
- package/.rulesync/skills/write-plan/SKILL.md +5 -84
- package/.rulesync/subagents/grunt.md +2 -24
- package/.rulesync/subagents/orchestrator.md +14 -59
- package/AGENTS.md +9 -47
- package/CHANGELOG.md +56 -0
- package/CLAUDE.md +1 -49
- package/README.md +88 -80
- package/cli/grunt.mjs +18 -2
- package/cli/grunt.test.ts +33 -0
- package/cli/init.mjs +103 -10
- package/cli/init.test.ts +147 -35
- package/cli/prompt.mjs +8 -0
- package/cli/prompt.test.ts +14 -0
- package/package.json +7 -2
- package/scripts/browser.mjs +98 -5
- package/scripts/doctor.mjs +82 -1
- package/scripts/emit-gemini.mjs +1 -1
- package/scripts/emit-mcp-policy.mjs +1 -1
- package/scripts/gate-fat-tools.mjs +223 -173
- package/scripts/google-workspace.mjs +713 -0
- package/scripts/grunt-job.mjs +453 -69
- package/scripts/listen.mjs +829 -0
- package/scripts/parse-need.mjs +21 -2
- package/scripts/persist-implementation.mjs +278 -0
- package/scripts/scrub-spawn-prompt.mjs +3 -50
- package/scripts/setup.mjs +746 -0
- package/scripts/speak.mjs +291 -0
- package/.agents/agents/implementer.md +0 -32
- package/.agents/agents/thinker.md +0 -29
- package/.agents/skills/cascade/SKILL.md +0 -28
- package/.agents/skills/parent/SKILL.md +0 -6
- package/.agents/skills/solo/SKILL.md +0 -25
- package/.claude/agents/implementer.md +0 -37
- package/.claude/agents/thinker.md +0 -36
- package/.claude/skills/cascade/SKILL.md +0 -28
- package/.claude/skills/parent/SKILL.md +0 -6
- package/.claude/skills/solo/SKILL.md +0 -25
- package/.codex/agents/implementer.toml +0 -26
- package/.codex/agents/thinker.toml +0 -26
- package/.grok/agents/implementer.md +0 -27
- package/.grok/agents/thinker.md +0 -27
- package/.grok/parent.md +0 -1
- package/.grok/roles/implementer.toml +0 -5
- package/.grok/roles/thinker.toml +0 -5
- package/.grok/skills/cascade/SKILL.md +0 -28
- package/.grok/skills/parent/SKILL.md +0 -6
- package/.grok/skills/solo/SKILL.md +0 -25
- package/.rulesync/grunt.config.jsonc +0 -7
- package/.rulesync/skills/cascade/SKILL.md +0 -28
- package/.rulesync/skills/parent/SKILL.md +0 -6
- package/.rulesync/skills/solo/SKILL.md +0 -25
- package/.rulesync/subagents/implementer.md +0 -49
- package/.rulesync/subagents/thinker.md +0 -49
- package/scripts/grunt-config.mjs +0 -50
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/** PreToolUse
|
|
3
|
-
|
|
4
|
-
SubagentStop intercepts need: search|exec with grunt-job verdicts.
|
|
5
|
-
Stop: first-non-empty-line [orchestrator]:/[grunt]:/[implementer]:/[thinker]:/[handoff]:/[tmp]: recap
|
|
6
|
-
| parent-escape once; else block. MAX_STOP=3. No isCheap/trivia.
|
|
7
|
-
Empty lastAssistantMessage → transcript_path tail-scan. Fail-open: parse/crash → empty stdout, exit 0.
|
|
2
|
+
/** PreToolUse fat-gate + persist rewrite. Stop writes session receipt. SubagentStop intercepts need: search|exec|slice|fetch.
|
|
3
|
+
Fail-open: parse/crash → empty stdout, exit 0.
|
|
8
4
|
*/
|
|
9
5
|
import fs from "node:fs";
|
|
10
6
|
import path from "node:path";
|
|
@@ -17,37 +13,21 @@ import {
|
|
|
17
13
|
import {
|
|
18
14
|
denyResponse,
|
|
19
15
|
fatHookOutput,
|
|
20
|
-
isWorkspaceGruntJob,
|
|
21
|
-
isParentOrchestrator,
|
|
22
16
|
processFatTools,
|
|
23
17
|
rewriteGruntScratchPath,
|
|
24
|
-
subagentTypeOf,
|
|
25
18
|
} from "../../scripts/gate-fat-tools.mjs";
|
|
26
19
|
import { persistPlan } from "../../scripts/persist-plan.mjs";
|
|
27
20
|
import { persistHandoff } from "../../scripts/persist-handoff.mjs";
|
|
21
|
+
import { persistImplementation, isFullImplementationBody, validateImplementation } from "../../scripts/persist-implementation.mjs";
|
|
28
22
|
import { persistTmp } from "../../scripts/persist-tmp.mjs";
|
|
29
23
|
import { parseNeed } from "../../scripts/parse-need.mjs";
|
|
30
24
|
import { resolveJobCwd, runJob } from "../../scripts/grunt-job.mjs";
|
|
31
|
-
import { loadLeftoverGate, loadSpawnMode } from "../../scripts/grunt-config.mjs";
|
|
32
25
|
|
|
33
26
|
export const ORCHESTRATOR_LOGS_DIR = ".tmp/grunt/orchestrator-logs";
|
|
34
27
|
/** One-release dual-read; drop next release. */
|
|
35
28
|
export const LEGACY_ORCHESTRATOR_LOGS_DIR = ".tmp/orchestrator-logs";
|
|
36
|
-
export const DENY_REASON =
|
|
37
|
-
"First action=spawn implementer|grunt|thinker. Deny expected. Only /solo this session escapes.";
|
|
38
|
-
export const STOP_REASONS = [
|
|
39
|
-
"Spawn: ⚠/validate/sim findings or writes remain after Implement pick/`/implement-plan`/explicit implement or parent just tried to Write → spawn implementer. Else facts → grunt. Else no spec/not small/simple → thinker then recap-stop. Else small/simple/defined → implementer. Thinker recap ≠ spec-ready. `ok`/`yes` ≠ implement. Else recap `[orchestrator]:` one recap line; advise leftover numbered pick each on own line after (echo printed leftover; always-print typed triple).",
|
|
40
|
-
"Still spawn: ⚠/validate/sim findings or writes remain after Implement pick/`/implement-plan`/explicit implement or parent just tried to Write → spawn implementer. Else facts → grunt. Else no spec/not small/simple → thinker then recap-stop. Else small/simple/defined → implementer. Thinker recap ≠ spec-ready. `ok`/`yes` ≠ implement. Else recap `[orchestrator]:` one recap line; advise leftover numbered pick each on own line after (echo printed leftover; always-print typed triple).",
|
|
41
|
-
"Last spawn: ⚠/validate/sim findings or writes remain after Implement pick/`/implement-plan`/explicit implement or parent just tried to Write → spawn implementer. Else facts → grunt. Else no spec/not small/simple → thinker then recap-stop. Else small/simple/defined → implementer. Thinker recap ≠ spec-ready. `ok`/`yes` ≠ implement. Else recap `[orchestrator]:` one recap line; advise leftover numbered pick each on own line after (echo printed leftover; always-print typed triple).",
|
|
42
|
-
];
|
|
29
|
+
export const DENY_REASON = "denied";
|
|
43
30
|
const TRANSCRIPT_TAIL_BYTES = 512 * 1024;
|
|
44
|
-
const PARENT_TOOLS = new Set([
|
|
45
|
-
"todowrite",
|
|
46
|
-
"getcommandorsubagentoutput",
|
|
47
|
-
"gettaskoutput",
|
|
48
|
-
"killcommandorsubagent",
|
|
49
|
-
"killtask",
|
|
50
|
-
]);
|
|
51
31
|
const SPAWN_TOOLS = new Set([
|
|
52
32
|
"spawnsubagent",
|
|
53
33
|
"task",
|
|
@@ -60,33 +40,16 @@ const WRITE_TOOLS = new Set([
|
|
|
60
40
|
"searchreplace",
|
|
61
41
|
"replacefilecontent",
|
|
62
42
|
]);
|
|
63
|
-
const
|
|
64
|
-
const SKILL_TOOLS = new Set(["skill"]);
|
|
65
|
-
export const PARENT_SKILLS = new Set([
|
|
66
|
-
"parent",
|
|
67
|
-
"explain",
|
|
68
|
-
"solo",
|
|
69
|
-
"cascade",
|
|
70
|
-
"auto",
|
|
71
|
-
"ask",
|
|
72
|
-
"handoff",
|
|
73
|
-
"tmp",
|
|
74
|
-
"pickup",
|
|
75
|
-
"write-plan",
|
|
76
|
-
"implement-plan",
|
|
77
|
-
]);
|
|
78
|
-
const INTERCEPT_JOBS = new Set(["search", "exec"]);
|
|
79
|
-
const MAX_STOP = 3;
|
|
43
|
+
const INTERCEPT_JOBS = new Set(["search", "exec", "slice", "fetch"]);
|
|
80
44
|
const MAX_INTERCEPT = 3;
|
|
81
|
-
|
|
82
|
-
const SOLO_RE = /^\s*\/solo\s*$/;
|
|
83
|
-
const CASCADE_RE = /^\s*\/cascade\s*$/;
|
|
45
|
+
const MAX_STOP = 3;
|
|
84
46
|
const AUTO_RE = /^\s*\/auto\s*$/;
|
|
85
47
|
const ASK_RE = /^\s*\/ask\s*$/;
|
|
86
|
-
const
|
|
48
|
+
export const SESSION_GATE_STAMP = "session-gate";
|
|
49
|
+
/** One-release dual-read of 0.5 leftover stamp. */
|
|
87
50
|
export const AUTO_ASK_STAMP = "auto-ask";
|
|
88
|
-
export const
|
|
89
|
-
|
|
51
|
+
export const ASK_STOP_REASON = "ask after this step";
|
|
52
|
+
const WAIT_GRUNT = "[orchestrator]: wait grunt";
|
|
90
53
|
|
|
91
54
|
function main() {
|
|
92
55
|
try {
|
|
@@ -97,7 +60,6 @@ function main() {
|
|
|
97
60
|
"",
|
|
98
61
|
);
|
|
99
62
|
if (event === "pretooluse") return preToolUse(data || {});
|
|
100
|
-
if (event === "posttooluse") return postToolUse(data || {});
|
|
101
63
|
if (event === "userpromptsubmit") return userPromptSubmit(data || {});
|
|
102
64
|
if (event === "stop") return stop(data || {});
|
|
103
65
|
if (event === "subagentstop") return interceptNeed(data || {}, "SubagentStop");
|
|
@@ -113,18 +75,6 @@ function preToolUse(data) {
|
|
|
113
75
|
let toolInput = data.toolInput;
|
|
114
76
|
if (toolInput == null) toolInput = data.tool_input;
|
|
115
77
|
|
|
116
|
-
const sub = subagentTypeOf(data);
|
|
117
|
-
if (sub || !isParentOrchestrator(data)) {
|
|
118
|
-
const code = emitFat(data);
|
|
119
|
-
return code == null ? 0 : code;
|
|
120
|
-
}
|
|
121
|
-
if (isSoloMode(data)) {
|
|
122
|
-
// Single-agent session: no parent-deny, no spawn rewrite. Fat gate still applies.
|
|
123
|
-
const fatCode = emitFat(data);
|
|
124
|
-
if (fatCode !== null) return fatCode;
|
|
125
|
-
emit({ decision: "allow" });
|
|
126
|
-
return 0;
|
|
127
|
-
}
|
|
128
78
|
if (SPAWN_TOOLS.has(toolKey)) {
|
|
129
79
|
const updated = rewriteSpawn(toolInput, data);
|
|
130
80
|
if (updated && updated.__denied) return 0;
|
|
@@ -141,43 +91,15 @@ function preToolUse(data) {
|
|
|
141
91
|
return 0;
|
|
142
92
|
}
|
|
143
93
|
if (WRITE_TOOLS.has(toolKey)) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (PARENT_TOOLS.has(toolKey)) {
|
|
147
|
-
emit({ decision: "allow" });
|
|
148
|
-
return 0;
|
|
94
|
+
const persistCode = parentWrite(data, toolInput);
|
|
95
|
+
if (persistCode !== null) return persistCode;
|
|
149
96
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
if (SKILL_TOOLS.has(toolKey) && isAllowedParentSkill(toolInput)) {
|
|
155
|
-
emit({ decision: "allow" });
|
|
156
|
-
return 0;
|
|
157
|
-
}
|
|
158
|
-
if (hasParentEscape(data)) {
|
|
159
|
-
const fatCode = emitFat(data);
|
|
160
|
-
if (fatCode !== null) return fatCode;
|
|
161
|
-
emit({ decision: "allow" });
|
|
162
|
-
return 0;
|
|
163
|
-
}
|
|
164
|
-
emit({ decision: "deny", reason: DENY_REASON });
|
|
97
|
+
const fatCode = emitFat(data);
|
|
98
|
+
if (fatCode !== null) return fatCode;
|
|
99
|
+
emit({ decision: "allow" });
|
|
165
100
|
return 0;
|
|
166
101
|
}
|
|
167
102
|
|
|
168
|
-
/** Session flag, not one-turn. Fail-closed: unreadable spawn-mode stamp is not solo. */
|
|
169
|
-
export function isSoloMode(data) {
|
|
170
|
-
try {
|
|
171
|
-
return spawnModeOf(data) === "solo";
|
|
172
|
-
} catch {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function hasParentEscape(data) {
|
|
178
|
-
return Boolean(resolveStamp(data, "parent-escape"));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
103
|
function emitFat(data) {
|
|
182
104
|
const fat = fatHookOutput(processFatTools(data));
|
|
183
105
|
if (!fat) return null;
|
|
@@ -214,11 +136,18 @@ export function isUnderHandoffs(filePath, workspaceRoot) {
|
|
|
214
136
|
return isUnderDir(filePath, workspaceRoot, [".tmp", "grunt", "handoffs"]);
|
|
215
137
|
}
|
|
216
138
|
|
|
139
|
+
export function isUnderImplementations(filePath, workspaceRoot) {
|
|
140
|
+
return isUnderDir(filePath, workspaceRoot, [".tmp", "grunt", "implementations"]);
|
|
141
|
+
}
|
|
142
|
+
|
|
217
143
|
export const TMP_RESERVED_DIRS = new Set([
|
|
218
144
|
"plans",
|
|
219
145
|
"handoffs",
|
|
146
|
+
"implementations",
|
|
220
147
|
"browser",
|
|
221
148
|
"orchestrator-logs",
|
|
149
|
+
"stash",
|
|
150
|
+
"sessions",
|
|
222
151
|
]);
|
|
223
152
|
|
|
224
153
|
export function isUnderTmp(filePath, workspaceRoot) {
|
|
@@ -231,44 +160,6 @@ export function isUnderTmp(filePath, workspaceRoot) {
|
|
|
231
160
|
return !TMP_RESERVED_DIRS.has(path.basename(abs));
|
|
232
161
|
}
|
|
233
162
|
|
|
234
|
-
export function isAllowedParentGruntJob(command, workspaceRoot) {
|
|
235
|
-
return isWorkspaceGruntJob(command, workspaceRoot, ["search", "exec"]);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function bashCommandOf(toolInput) {
|
|
239
|
-
if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput)) {
|
|
240
|
-
return "";
|
|
241
|
-
}
|
|
242
|
-
return String(toolInput.command ?? toolInput.cmd ?? "");
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function skillNameOf(toolInput) {
|
|
246
|
-
if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput)) {
|
|
247
|
-
return "";
|
|
248
|
-
}
|
|
249
|
-
const raw =
|
|
250
|
-
toolInput.skill ??
|
|
251
|
-
toolInput.skill_name ??
|
|
252
|
-
toolInput.skillName ??
|
|
253
|
-
toolInput.name ??
|
|
254
|
-
"";
|
|
255
|
-
const s = String(raw).trim().toLowerCase().replace(/\\/g, "/");
|
|
256
|
-
const base = s.split("/").pop() || "";
|
|
257
|
-
return base.replace(/\.md$/, "").replace(/_/g, "-");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export function isAllowedParentSkill(toolInput) {
|
|
261
|
-
return PARENT_SKILLS.has(skillNameOf(toolInput));
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function isHostStopBanner(prompt) {
|
|
265
|
-
const p = String(prompt || "");
|
|
266
|
-
if (/^\s*Stop hook feedback:/.test(p)) return true;
|
|
267
|
-
if (/Blocked by stop hook/.test(p)) return true;
|
|
268
|
-
if (/task[-_ ]?notification/i.test(p)) return true;
|
|
269
|
-
return false;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
163
|
function parentWrite(data, toolInput) {
|
|
273
164
|
if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput)) {
|
|
274
165
|
emit({ decision: "deny", reason: DENY_REASON });
|
|
@@ -291,16 +182,44 @@ function parentWrite(data, toolInput) {
|
|
|
291
182
|
} else if (isUnderHandoffs(rawPath, ws)) {
|
|
292
183
|
persist = persistHandoff;
|
|
293
184
|
invalid = "invalid handoff";
|
|
185
|
+
} else if (isUnderImplementations(rawPath, ws)) {
|
|
186
|
+
persist = persistImplementation;
|
|
187
|
+
invalid = "invalid implementation";
|
|
188
|
+
const abs = path.isAbsolute(rawPath)
|
|
189
|
+
? path.resolve(rawPath)
|
|
190
|
+
: path.resolve(ws, rawPath);
|
|
191
|
+
const content = typeof toolInput.content === "string" ? toolInput.content : "";
|
|
192
|
+
const named = /^\s*IMPL_NAME:/m.test(content);
|
|
193
|
+
let exists = false;
|
|
194
|
+
try {
|
|
195
|
+
exists = fs.statSync(abs).isFile();
|
|
196
|
+
} catch {
|
|
197
|
+
exists = false;
|
|
198
|
+
}
|
|
199
|
+
if (!named && exists) {
|
|
200
|
+
if (!isFullImplementationBody(content)) return null;
|
|
201
|
+
const errors = validateImplementation(path.basename(abs), content);
|
|
202
|
+
if (errors.length) {
|
|
203
|
+
emit({ decision: "deny", reason: errors.join("; ") || invalid });
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
const next = Object.assign({}, toolInput, {
|
|
207
|
+
file_path: abs,
|
|
208
|
+
content,
|
|
209
|
+
});
|
|
210
|
+
emit({
|
|
211
|
+
hookSpecificOutput: {
|
|
212
|
+
hookEventName: "PreToolUse",
|
|
213
|
+
updatedInput: next,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
294
218
|
} else if (isUnderTmp(rawPath, ws)) {
|
|
295
219
|
persist = persistTmp;
|
|
296
220
|
invalid = "invalid tmp";
|
|
297
221
|
} else {
|
|
298
|
-
|
|
299
|
-
emit({ decision: "allow" });
|
|
300
|
-
return 0;
|
|
301
|
-
}
|
|
302
|
-
emit({ decision: "deny", reason: DENY_REASON });
|
|
303
|
-
return 0;
|
|
222
|
+
return null;
|
|
304
223
|
}
|
|
305
224
|
const content = typeof toolInput.content === "string" ? toolInput.content : "";
|
|
306
225
|
const result = persist({ workspaceRoot: ws, content });
|
|
@@ -325,7 +244,6 @@ function parentWrite(data, toolInput) {
|
|
|
325
244
|
}
|
|
326
245
|
|
|
327
246
|
function rewriteSpawn(toolInput, data) {
|
|
328
|
-
// Type default + intent-scrub in one updatedInput (last-wins with scrub-spawn).
|
|
329
247
|
const updated = rewriteSpawnToolInput(toolInput, { defaultGrunt: true });
|
|
330
248
|
const prompt =
|
|
331
249
|
updated && typeof updated.prompt === "string"
|
|
@@ -340,72 +258,93 @@ function rewriteSpawn(toolInput, data) {
|
|
|
340
258
|
return updated;
|
|
341
259
|
}
|
|
342
260
|
|
|
343
|
-
function postToolUse(data) {
|
|
344
|
-
if (data.subagentType || data.subagent_type) return 0;
|
|
345
|
-
writeStamp(data, "tools-used", "1");
|
|
346
|
-
return 0;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
261
|
function userPromptOf(data) {
|
|
350
262
|
if (!data || typeof data !== "object") return "";
|
|
351
263
|
return String(data.prompt ?? data.userPrompt ?? data.user_prompt ?? data.content ?? "");
|
|
352
264
|
}
|
|
353
265
|
|
|
354
|
-
function
|
|
355
|
-
|
|
266
|
+
function isHostStopBanner(prompt) {
|
|
267
|
+
const p = String(prompt || "");
|
|
268
|
+
if (/^\s*Stop hook feedback:/.test(p)) return true;
|
|
269
|
+
if (/Blocked by stop hook/.test(p)) return true;
|
|
270
|
+
if (/task[-_ ]?notification/i.test(p)) return true;
|
|
271
|
+
return false;
|
|
356
272
|
}
|
|
357
273
|
|
|
358
|
-
function
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const cfg = loadSpawnMode(workspaceRootOf(data));
|
|
368
|
-
if (token === cfg) {
|
|
369
|
-
unlinkSpawnModeStamp(data);
|
|
370
|
-
} else {
|
|
371
|
-
writeSpawnModeStamp(data, token);
|
|
372
|
-
}
|
|
373
|
-
unlinkSoloStamp(data);
|
|
374
|
-
}
|
|
375
|
-
if (AUTO_RE.test(prompt) || ASK_RE.test(prompt)) {
|
|
376
|
-
const token = AUTO_RE.test(prompt) ? "auto" : "ask";
|
|
377
|
-
const cfg = loadLeftoverGate(workspaceRootOf(data));
|
|
378
|
-
if (token === cfg) {
|
|
379
|
-
unlinkAutoAskStamp(data);
|
|
380
|
-
} else {
|
|
381
|
-
writeAutoAskStamp(data, token);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if (isParentEscapePrompt(prompt)) {
|
|
385
|
-
writeStamp(data, "parent-escape", "1");
|
|
386
|
-
} else {
|
|
387
|
-
unlinkStamp(data, "parent-escape");
|
|
274
|
+
function countLines(file) {
|
|
275
|
+
try {
|
|
276
|
+
return fs
|
|
277
|
+
.readFileSync(file, "utf8")
|
|
278
|
+
.split("\n")
|
|
279
|
+
.map((l) => l.trim())
|
|
280
|
+
.filter(Boolean).length;
|
|
281
|
+
} catch {
|
|
282
|
+
return 0;
|
|
388
283
|
}
|
|
389
|
-
return 0;
|
|
390
284
|
}
|
|
391
285
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
286
|
+
function sessionReceiptContext(data) {
|
|
287
|
+
const sid = sessionIdOf(data);
|
|
288
|
+
if (!sid || sid === "default") return "";
|
|
289
|
+
const dir = path.join(
|
|
290
|
+
workspaceRootOf(data),
|
|
291
|
+
".tmp",
|
|
292
|
+
"grunt",
|
|
293
|
+
"sessions",
|
|
294
|
+
sid,
|
|
295
|
+
);
|
|
296
|
+
if (!fs.existsSync(dir)) return "";
|
|
297
|
+
const wrote = countLines(path.join(dir, "wrote.txt"));
|
|
298
|
+
const read = countLines(path.join(dir, "read.txt"));
|
|
299
|
+
return `${wrote} wrote, ${read} read. session=.tmp/grunt/sessions/${sid}/`;
|
|
300
|
+
}
|
|
395
301
|
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
302
|
+
function writeSessionReceipt(data, msg) {
|
|
303
|
+
const sid = sessionIdOf(data);
|
|
304
|
+
if (!sid || sid === "default") return;
|
|
305
|
+
const dir = path.join(
|
|
306
|
+
workspaceRootOf(data),
|
|
307
|
+
".tmp",
|
|
308
|
+
"grunt",
|
|
309
|
+
"sessions",
|
|
310
|
+
sid,
|
|
311
|
+
);
|
|
312
|
+
try {
|
|
313
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
314
|
+
} catch {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const wrote = countLines(path.join(dir, "wrote.txt"));
|
|
318
|
+
const read = countLines(path.join(dir, "read.txt"));
|
|
319
|
+
const first = firstNonEmptyStripped(msg).slice(0, 200);
|
|
320
|
+
const lines = [
|
|
321
|
+
first,
|
|
322
|
+
`${wrote} wrote, ${read} read.`,
|
|
323
|
+
`session=.tmp/grunt/sessions/${sid}/`,
|
|
324
|
+
].filter(Boolean);
|
|
325
|
+
try {
|
|
326
|
+
fs.writeFileSync(path.join(dir, "receipt.txt"), lines.join("\n") + "\n");
|
|
327
|
+
} catch {
|
|
328
|
+
/* fail-open */
|
|
400
329
|
}
|
|
401
|
-
return "";
|
|
402
330
|
}
|
|
403
331
|
|
|
404
|
-
|
|
405
|
-
|
|
332
|
+
function userPromptSubmit(data) {
|
|
333
|
+
const prompt = userPromptOf(data);
|
|
334
|
+
if (isHostStopBanner(prompt)) return 0;
|
|
335
|
+
unlinkStamp(data, "stop-block");
|
|
336
|
+
applySessionGateSlash(data, prompt);
|
|
337
|
+
const ctx = effectiveGruntContext(data);
|
|
338
|
+
if (!ctx) return 0;
|
|
339
|
+
emit({
|
|
340
|
+
hookSpecificOutput: {
|
|
341
|
+
hookEventName: "UserPromptSubmit",
|
|
342
|
+
additionalContext: ctx,
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
return 0;
|
|
406
346
|
}
|
|
407
347
|
|
|
408
|
-
/** Mid-turn wait must be exactly `[orchestrator]: wait grunt` (no leftover lines). */
|
|
409
348
|
export function isWaitGruntExact(msg) {
|
|
410
349
|
const nonempty = String(msg || "")
|
|
411
350
|
.split("\n")
|
|
@@ -414,36 +353,21 @@ export function isWaitGruntExact(msg) {
|
|
|
414
353
|
return nonempty[0].replace(/^[\s`*_>]+/, "") === WAIT_GRUNT;
|
|
415
354
|
}
|
|
416
355
|
|
|
417
|
-
function
|
|
356
|
+
export function hasStepAsk(msg) {
|
|
357
|
+
if (isWaitGruntExact(msg)) return true;
|
|
418
358
|
return String(msg || "")
|
|
419
359
|
.split("\n")
|
|
420
|
-
.
|
|
360
|
+
.map((l) => l.replace(/^[\s`*_>]+/, "").trim())
|
|
361
|
+
.filter(Boolean)
|
|
362
|
+
.some((l) => l.includes("?"));
|
|
421
363
|
}
|
|
422
364
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const tweak = hit(/^3\.\s+Tweak$/);
|
|
430
|
-
const impl =
|
|
431
|
-
hit(/^1\.\s+Implement with verbal plan$/) &&
|
|
432
|
-
hit(/^2\.\s+Implement with file plan$/);
|
|
433
|
-
const write =
|
|
434
|
-
hit(/^1\.\s+Write with verbal plan$/) &&
|
|
435
|
-
hit(/^2\.\s+Write with file plan$/);
|
|
436
|
-
return Boolean(tweak && (impl || write));
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/** Ask leftover-required: any `[thinker]:`; `[orchestrator]:` iff >1 nonempty line (except exact wait-grunt); `[grunt]|[implementer]|[handoff]|[tmp]:` exempt. Auto waives in Stop. */
|
|
440
|
-
export function leftoverRequiredAsk(msg) {
|
|
441
|
-
if (isWaitGruntExact(msg)) return false;
|
|
442
|
-
const first = firstNonEmptyStripped(msg);
|
|
443
|
-
if (/^\[(?:grunt|implementer|handoff|tmp)\]:/.test(first)) return false;
|
|
444
|
-
if (/^\[thinker\]:/.test(first)) return true;
|
|
445
|
-
if (/^\[orchestrator\]:/.test(first)) return nonemptyLines(msg).length > 1;
|
|
446
|
-
return false;
|
|
365
|
+
function firstNonEmptyStripped(msg) {
|
|
366
|
+
for (const line of String(msg || "").split("\n")) {
|
|
367
|
+
if (!line.trim()) continue;
|
|
368
|
+
return line.replace(/^[\s`*_>]+/, "");
|
|
369
|
+
}
|
|
370
|
+
return "";
|
|
447
371
|
}
|
|
448
372
|
|
|
449
373
|
function payloadAssistantMessage(data) {
|
|
@@ -533,43 +457,21 @@ function stop(data) {
|
|
|
533
457
|
const reason = String(data.reason || "");
|
|
534
458
|
if (reason && reason !== "end_turn") return 0;
|
|
535
459
|
|
|
536
|
-
// Before the parent-escape consume: solo must never burn the one-turn stamp.
|
|
537
|
-
if (isSoloMode(data)) return 0;
|
|
538
|
-
|
|
539
|
-
if (resolveStamp(data, "parent-escape")) {
|
|
540
|
-
unlinkStamp(data, "parent-escape");
|
|
541
|
-
return 0;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
460
|
const payloadMsg = payloadAssistantMessage(data);
|
|
545
461
|
let msg = payloadMsg;
|
|
546
462
|
if (!msg) {
|
|
547
463
|
const tp = data.transcript_path || data.transcriptPath || "";
|
|
548
464
|
msg = lastAssistantFromTranscript(tp);
|
|
549
465
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
leftoverRequiredAsk(msg) &&
|
|
558
|
-
!hasLeftoverTriple(msg)
|
|
559
|
-
) {
|
|
560
|
-
// fall through to block — leftover-required under ask
|
|
561
|
-
} else {
|
|
562
|
-
return 0;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
let n = readStampInt(data, "stop-block");
|
|
567
|
-
if (n >= MAX_STOP) {
|
|
466
|
+
writeSessionReceipt(data, msg);
|
|
467
|
+
if (isWaitGruntExact(msg)) return 0;
|
|
468
|
+
if (sessionGateOf(data) === "ask" && !hasStepAsk(msg)) {
|
|
469
|
+
let n = readStampInt(data, "stop-block");
|
|
470
|
+
if (n >= MAX_STOP) return 0;
|
|
471
|
+
writeStamp(data, "stop-block", String(n + 1));
|
|
472
|
+
emit({ decision: "block", reason: ASK_STOP_REASON });
|
|
568
473
|
return 0;
|
|
569
474
|
}
|
|
570
|
-
writeStamp(data, "stop-block", String(n + 1));
|
|
571
|
-
const reasonText = STOP_REASONS[Math.min(n, STOP_REASONS.length - 1)];
|
|
572
|
-
emit({ decision: "block", reason: reasonText });
|
|
573
475
|
return 0;
|
|
574
476
|
}
|
|
575
477
|
|
|
@@ -611,6 +513,9 @@ function interceptNeed(data, hookEventName) {
|
|
|
611
513
|
cwd: jobCwd || cwd,
|
|
612
514
|
path: job.path,
|
|
613
515
|
glob: job.glob,
|
|
516
|
+
stash: job.stash,
|
|
517
|
+
from: job.from,
|
|
518
|
+
to: job.to,
|
|
614
519
|
});
|
|
615
520
|
}
|
|
616
521
|
} catch {
|
|
@@ -663,11 +568,6 @@ function writeStamp(data, prefix, body) {
|
|
|
663
568
|
return p;
|
|
664
569
|
}
|
|
665
570
|
|
|
666
|
-
function unlinkStamp(data, prefix) {
|
|
667
|
-
unlinkQuiet(stampPath(data, prefix));
|
|
668
|
-
unlinkQuiet(legacyStampPath(data, prefix));
|
|
669
|
-
}
|
|
670
|
-
|
|
671
571
|
function readStampInt(data, prefix) {
|
|
672
572
|
const p = resolveStamp(data, prefix);
|
|
673
573
|
if (!p) return 0;
|
|
@@ -684,142 +584,74 @@ function sessionIdOf(data) {
|
|
|
684
584
|
);
|
|
685
585
|
}
|
|
686
586
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const root = workspaceRootOf(data);
|
|
690
|
-
const sid = sessionIdOf(data);
|
|
691
|
-
if (!root || !sid) return null;
|
|
692
|
-
return path.join(root, ORCHESTRATOR_LOGS_DIR, SOLO_STAMP + "-" + sid);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function legacySoloStampPath(data) {
|
|
696
|
-
const root = workspaceRootOf(data);
|
|
697
|
-
const sid = sessionIdOf(data);
|
|
698
|
-
if (!root || !sid) return null;
|
|
699
|
-
return path.join(root, LEGACY_ORCHESTRATOR_LOGS_DIR, SOLO_STAMP + "-" + sid);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function resolveSoloStamp(data) {
|
|
703
|
-
const neu = soloStampPath(data);
|
|
704
|
-
if (neu && fs.existsSync(neu)) return neu;
|
|
705
|
-
const old = legacySoloStampPath(data);
|
|
706
|
-
if (old && fs.existsSync(old)) return old;
|
|
707
|
-
return null;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function unlinkSoloStamp(data) {
|
|
711
|
-
unlinkQuiet(soloStampPath(data));
|
|
712
|
-
unlinkQuiet(legacySoloStampPath(data));
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/** Spawn-mode stamp: never share a `default` stamp across sid-less sessions. */
|
|
716
|
-
export function spawnModeStampPath(data) {
|
|
717
|
-
const root = workspaceRootOf(data);
|
|
718
|
-
const sid = sessionIdOf(data);
|
|
719
|
-
if (!root || !sid) return null;
|
|
720
|
-
return path.join(root, ORCHESTRATOR_LOGS_DIR, SPAWN_MODE_STAMP + "-" + sid);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function legacySpawnModeStampPath(data) {
|
|
724
|
-
const root = workspaceRootOf(data);
|
|
725
|
-
const sid = sessionIdOf(data);
|
|
726
|
-
if (!root || !sid) return null;
|
|
727
|
-
return path.join(root, LEGACY_ORCHESTRATOR_LOGS_DIR, SPAWN_MODE_STAMP + "-" + sid);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function resolveSpawnModeStamp(data) {
|
|
731
|
-
const neu = spawnModeStampPath(data);
|
|
732
|
-
if (neu && fs.existsSync(neu)) return neu;
|
|
733
|
-
const old = legacySpawnModeStampPath(data);
|
|
734
|
-
if (old && fs.existsSync(old)) return old;
|
|
735
|
-
return null;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function writeSpawnModeStamp(data, body) {
|
|
739
|
-
const p = spawnModeStampPath(data);
|
|
740
|
-
if (!p) return null;
|
|
741
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
742
|
-
fs.writeFileSync(p, body);
|
|
743
|
-
return p;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function unlinkSpawnModeStamp(data) {
|
|
747
|
-
unlinkQuiet(spawnModeStampPath(data));
|
|
748
|
-
unlinkQuiet(legacySpawnModeStampPath(data));
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/** Valid stamp solo|cascade wins; else grunt-off presence as solo; else config. Unreadable/bad body ignored. */
|
|
752
|
-
export function spawnModeOf(data) {
|
|
587
|
+
function unlinkQuiet(p) {
|
|
588
|
+
if (!p) return;
|
|
753
589
|
try {
|
|
754
|
-
|
|
755
|
-
if (p) {
|
|
756
|
-
const body = fs.readFileSync(p, "utf8").trim();
|
|
757
|
-
if (body === "solo" || body === "cascade") return body;
|
|
758
|
-
}
|
|
590
|
+
fs.unlinkSync(p);
|
|
759
591
|
} catch {
|
|
760
|
-
|
|
592
|
+
/* missing is fine */
|
|
761
593
|
}
|
|
762
|
-
if (resolveSoloStamp(data)) return "solo";
|
|
763
|
-
return loadSpawnMode(workspaceRootOf(data));
|
|
764
594
|
}
|
|
765
595
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const sid = sessionIdOf(data);
|
|
770
|
-
if (!root || !sid) return null;
|
|
771
|
-
return path.join(root, ORCHESTRATOR_LOGS_DIR, AUTO_ASK_STAMP + "-" + sid);
|
|
596
|
+
function unlinkStamp(data, prefix) {
|
|
597
|
+
unlinkQuiet(stampPath(data, prefix));
|
|
598
|
+
unlinkQuiet(legacyStampPath(data, prefix));
|
|
772
599
|
}
|
|
773
600
|
|
|
774
|
-
function
|
|
601
|
+
function sessionGateStampPath(data, prefix) {
|
|
775
602
|
const root = workspaceRootOf(data);
|
|
776
603
|
const sid = sessionIdOf(data);
|
|
777
|
-
if (!root || !sid) return null;
|
|
778
|
-
return path.join(root,
|
|
604
|
+
if (!root || !sid || sid === "default") return null;
|
|
605
|
+
return path.join(root, ORCHESTRATOR_LOGS_DIR, prefix + "-" + sid);
|
|
779
606
|
}
|
|
780
607
|
|
|
781
|
-
function
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
608
|
+
function resolveSessionGateStamp(data) {
|
|
609
|
+
for (const prefix of [SESSION_GATE_STAMP, AUTO_ASK_STAMP]) {
|
|
610
|
+
const neu = sessionGateStampPath(data, prefix);
|
|
611
|
+
if (neu && fs.existsSync(neu)) return neu;
|
|
612
|
+
const old = path.join(
|
|
613
|
+
workspaceRootOf(data),
|
|
614
|
+
LEGACY_ORCHESTRATOR_LOGS_DIR,
|
|
615
|
+
prefix + "-" + sessionIdOf(data),
|
|
616
|
+
);
|
|
617
|
+
if (sessionIdOf(data) && fs.existsSync(old)) return old;
|
|
618
|
+
}
|
|
786
619
|
return null;
|
|
787
620
|
}
|
|
788
621
|
|
|
789
|
-
function
|
|
790
|
-
|
|
791
|
-
if (
|
|
622
|
+
function applySessionGateSlash(data, prompt) {
|
|
623
|
+
let slash = "";
|
|
624
|
+
if (AUTO_RE.test(prompt)) slash = "auto";
|
|
625
|
+
else if (ASK_RE.test(prompt)) slash = "ask";
|
|
626
|
+
if (!slash) return;
|
|
627
|
+
const p = sessionGateStampPath(data, SESSION_GATE_STAMP);
|
|
628
|
+
if (!p) return;
|
|
629
|
+
unlinkQuiet(sessionGateStampPath(data, AUTO_ASK_STAMP));
|
|
630
|
+
if (slash === "auto") {
|
|
631
|
+
unlinkQuiet(p);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
792
634
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
793
|
-
fs.writeFileSync(p,
|
|
794
|
-
return p;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
function unlinkAutoAskStamp(data) {
|
|
798
|
-
unlinkQuiet(autoAskStampPath(data));
|
|
799
|
-
unlinkQuiet(legacyAutoAskStampPath(data));
|
|
635
|
+
fs.writeFileSync(p, slash);
|
|
800
636
|
}
|
|
801
637
|
|
|
802
|
-
|
|
803
|
-
export function leftoverGateOf(data) {
|
|
638
|
+
export function sessionGateOf(data) {
|
|
804
639
|
try {
|
|
805
|
-
const p =
|
|
640
|
+
const p = resolveSessionGateStamp(data);
|
|
806
641
|
if (p) {
|
|
807
642
|
const body = fs.readFileSync(p, "utf8").trim();
|
|
808
643
|
if (body === "auto" || body === "ask") return body;
|
|
809
644
|
}
|
|
810
645
|
} catch {
|
|
811
|
-
|
|
646
|
+
/* fall through */
|
|
812
647
|
}
|
|
813
|
-
return
|
|
648
|
+
return "auto";
|
|
814
649
|
}
|
|
815
650
|
|
|
816
|
-
function
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
} catch {
|
|
821
|
-
// missing is fine
|
|
822
|
-
}
|
|
651
|
+
export function effectiveGruntContext(data) {
|
|
652
|
+
const gate = sessionGateOf(data);
|
|
653
|
+
const receipt = sessionReceiptContext(data);
|
|
654
|
+
return receipt ? `sessionGate=${gate}. ${receipt}` : `sessionGate=${gate}`;
|
|
823
655
|
}
|
|
824
656
|
|
|
825
657
|
function eventKey(s) {
|