@brawnen/agent-harness-cli 0.1.0 → 0.1.1

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.
@@ -0,0 +1,130 @@
1
+ import { firstDefined, firstString } from "./shared.js";
2
+
3
+ export function buildGeminiHookOutput(decision) {
4
+ if (decision.status === "block") {
5
+ return {
6
+ decision: "deny",
7
+ reason: decision.reason
8
+ };
9
+ }
10
+
11
+ if (!decision.additionalContext) {
12
+ return {};
13
+ }
14
+
15
+ return {
16
+ hookSpecificOutput: {
17
+ additionalContext: decision.additionalContext
18
+ }
19
+ };
20
+ }
21
+
22
+ export function resolveGeminiCompletionMessage(payload) {
23
+ return firstString([
24
+ payload?.prompt_response,
25
+ payload?.response,
26
+ payload?.last_assistant_message
27
+ ]) ?? "";
28
+ }
29
+
30
+ export function resolveGeminiToolName(payload) {
31
+ return firstString([
32
+ payload?.tool_name,
33
+ payload?.toolName,
34
+ payload?.tool?.name,
35
+ payload?.toolUse?.name,
36
+ payload?.name
37
+ ]);
38
+ }
39
+
40
+ export function resolveGeminiToolCommand(payload) {
41
+ return firstString([
42
+ payload?.tool_input?.command,
43
+ payload?.toolInput?.command,
44
+ payload?.input?.command,
45
+ payload?.arguments?.command,
46
+ payload?.tool_use?.input?.command,
47
+ payload?.toolUse?.input?.command,
48
+ payload?.command
49
+ ]) ?? "";
50
+ }
51
+
52
+ export function resolveGeminiToolPath(payload) {
53
+ return firstString([
54
+ payload?.tool_input?.file_path,
55
+ payload?.tool_input?.path,
56
+ payload?.toolInput?.file_path,
57
+ payload?.toolInput?.path,
58
+ payload?.input?.file_path,
59
+ payload?.input?.path,
60
+ payload?.arguments?.file_path,
61
+ payload?.arguments?.path,
62
+ payload?.tool_use?.input?.file_path,
63
+ payload?.tool_use?.input?.path,
64
+ payload?.toolUse?.input?.file_path,
65
+ payload?.toolUse?.input?.path
66
+ ]);
67
+ }
68
+
69
+ export function resolveGeminiToolExitCode(payload) {
70
+ const value = firstDefined([
71
+ payload?.exit_code,
72
+ payload?.exitCode,
73
+ payload?.result?.exit_code,
74
+ payload?.result?.exitCode,
75
+ payload?.tool_response?.exit_code,
76
+ payload?.tool_response?.exitCode,
77
+ payload?.toolResponse?.exit_code,
78
+ payload?.toolResponse?.exitCode,
79
+ payload?.status
80
+ ]);
81
+
82
+ if (typeof value === "number") {
83
+ return value;
84
+ }
85
+
86
+ if (typeof value === "string" && value.trim() !== "") {
87
+ const parsed = Number(value);
88
+ return Number.isFinite(parsed) ? parsed : null;
89
+ }
90
+
91
+ return null;
92
+ }
93
+
94
+ export function resolveGeminiToolOutput(payload) {
95
+ const directOutput = firstString([
96
+ payload?.tool_response?.output,
97
+ payload?.tool_response?.stdout,
98
+ payload?.toolResponse?.output,
99
+ payload?.toolResponse?.stdout,
100
+ payload?.stdout,
101
+ payload?.stderr,
102
+ payload?.result?.output,
103
+ payload?.result?.stdout,
104
+ payload?.output
105
+ ]);
106
+
107
+ if (directOutput) {
108
+ return directOutput;
109
+ }
110
+
111
+ const displayOutput = firstString([
112
+ payload?.tool_response?.returnDisplay,
113
+ payload?.toolResponse?.returnDisplay
114
+ ]);
115
+
116
+ if (displayOutput) {
117
+ return displayOutput;
118
+ }
119
+
120
+ const responsePayload = firstDefined([
121
+ payload?.tool_response,
122
+ payload?.toolResponse
123
+ ]);
124
+
125
+ if (!responsePayload || typeof responsePayload === "string") {
126
+ return responsePayload ?? "";
127
+ }
128
+
129
+ return JSON.stringify(responsePayload);
130
+ }
@@ -0,0 +1,52 @@
1
+ import fs from "node:fs";
2
+
3
+ import { resolveHarnessProjectRoot } from "../runtime-paths.js";
4
+
5
+ export function readHookPayload() {
6
+ const raw = fs.readFileSync(0, "utf8").trim();
7
+ if (!raw) {
8
+ return {};
9
+ }
10
+
11
+ try {
12
+ return JSON.parse(raw);
13
+ } catch {
14
+ throw new Error("hook stdin 不是合法 JSON");
15
+ }
16
+ }
17
+
18
+ export function resolvePayloadCwd(payload) {
19
+ return resolveHarnessProjectRoot(payload?.cwd || process.cwd());
20
+ }
21
+
22
+ export function resolvePayloadPrompt(payload) {
23
+ if (typeof payload?.prompt === "string") {
24
+ return payload.prompt;
25
+ }
26
+ if (typeof payload?.input === "string") {
27
+ return payload.input;
28
+ }
29
+ return "";
30
+ }
31
+
32
+ export function firstDefined(values) {
33
+ for (const value of values) {
34
+ if (value !== undefined && value !== null) {
35
+ return value;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+
41
+ export function firstString(values) {
42
+ for (const value of values) {
43
+ if (typeof value === "string" && value.trim().length > 0) {
44
+ return value.trim();
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+
50
+ export function writeHookOutput(result) {
51
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
52
+ }
@@ -4,6 +4,13 @@ import path from "node:path";
4
4
  export const DEFAULT_RUNTIME_DIR = ".harness";
5
5
  export const LEGACY_RUNTIME_DIR = "harness";
6
6
 
7
+ const STRONG_PROJECT_ROOT_MARKERS = [
8
+ "harness.yaml",
9
+ ".codex/hooks.json",
10
+ ".claude/settings.json",
11
+ ".gemini/settings.json"
12
+ ];
13
+
7
14
  export function resolveRuntimeDirName(cwd, options = {}) {
8
15
  const preferExisting = options.preferExisting !== false;
9
16
  if (preferExisting) {
@@ -17,6 +24,26 @@ export function resolveRuntimeDirName(cwd, options = {}) {
17
24
  return DEFAULT_RUNTIME_DIR;
18
25
  }
19
26
 
27
+ export function resolveHarnessProjectRoot(cwd) {
28
+ const fallback = path.resolve(cwd || process.cwd());
29
+ let current = fallback;
30
+ let bestCandidate = null;
31
+
32
+ while (true) {
33
+ const score = getProjectRootScore(current);
34
+ if (score > 0 && (!bestCandidate || score > bestCandidate.score)) {
35
+ bestCandidate = { path: current, score };
36
+ }
37
+
38
+ const parent = path.dirname(current);
39
+ if (parent === current) {
40
+ return bestCandidate?.path ?? fallback;
41
+ }
42
+
43
+ current = parent;
44
+ }
45
+ }
46
+
20
47
  export function runtimePath(cwd, ...segments) {
21
48
  return path.join(cwd, resolveRuntimeDirName(cwd), ...segments);
22
49
  }
@@ -44,3 +71,15 @@ export function runtimeRelativeCandidates(...segments) {
44
71
  export function hasRuntimeSetup(cwd) {
45
72
  return runtimeRelativeCandidates().some((relativePath) => fs.existsSync(path.join(cwd, relativePath)));
46
73
  }
74
+
75
+ function getProjectRootScore(cwd) {
76
+ if (STRONG_PROJECT_ROOT_MARKERS.some((relativePath) => fs.existsSync(path.join(cwd, relativePath)))) {
77
+ return 2;
78
+ }
79
+
80
+ if (fs.existsSync(path.join(cwd, DEFAULT_RUNTIME_DIR)) || fs.existsSync(path.join(cwd, LEGACY_RUNTIME_DIR))) {
81
+ return 1;
82
+ }
83
+
84
+ return 0;
85
+ }
@@ -25,6 +25,12 @@ const CONTINUE_KEYWORDS = [
25
25
  "follow-up"
26
26
  ];
27
27
 
28
+ const AFFIRMATIVE_SHORT_REPLIES = [
29
+ "ok", "okay", "好", "好的", "嗯", "对", "是的", "可以",
30
+ "行", "没问题", "同意", "确认", "yes", "yep", "sure", "lgtm",
31
+ "先这样", "就这样", "开始吧", "搞起", "go"
32
+ ];
33
+
28
34
  const NEW_TASK_KEYWORDS = [
29
35
  "新任务",
30
36
  "另一个问题",
@@ -245,7 +251,7 @@ export function autoIntakePrompt(cwd, prompt) {
245
251
  return {
246
252
  action: "continue",
247
253
  task: activeTask,
248
- additionalContext: buildCurrentTaskContext(activeTask),
254
+ additionalContext: "",
249
255
  decision
250
256
  };
251
257
  }
@@ -389,6 +395,15 @@ function classifyPromptAgainstTask(prompt, activeTask) {
389
395
  });
390
396
  }
391
397
 
398
+ if (isAffirmativeShortReply(normalizedPrompt)) {
399
+ return buildDecision("continue", {
400
+ reasonCode: "affirmative_short_reply",
401
+ reason: "输入为确认性短回复,视为延续当前任务。",
402
+ matchedSignals: ["affirmative_short_reply"],
403
+ confidence: "medium"
404
+ });
405
+ }
406
+
392
407
  const ambiguous = isAmbiguousPrompt(prompt);
393
408
  if (ambiguous) {
394
409
  const matchedHighRiskKeyword = findMatchedKeyword(normalizedPrompt, HIGH_RISK_KEYWORDS);
@@ -397,8 +412,8 @@ function classifyPromptAgainstTask(prompt, activeTask) {
397
412
  block: highRisk,
398
413
  reasonCode: highRisk ? "ambiguous_high_risk_prompt" : "ambiguous_prompt",
399
414
  reason: highRisk
400
- ? "当前输入任务归属不明且包含高风险信号。先澄清是在延续旧任务还是新任务,再继续执行。"
401
- : "当前输入无法可靠判断是在延续旧任务还是新任务。先向用户澄清任务归属,再继续执行。",
415
+ ? "输入任务归属不明且含高风险信号,请先澄清。"
416
+ : "输入归属不明,请先澄清是延续当前任务还是新任务。",
402
417
  matchedSignals: highRisk
403
418
  ? [`high_risk_keyword:${matchedHighRiskKeyword}`, "ambiguous_prompt"]
404
419
  : ["ambiguous_prompt"],
@@ -428,6 +443,11 @@ function isAmbiguousPrompt(prompt) {
428
443
  return ["看一下这个", "有个问题", "帮我处理一下", "处理下", "看看这个"].some((text) => normalized.includes(text));
429
444
  }
430
445
 
446
+ function isAffirmativeShortReply(normalizedPrompt) {
447
+ const trimmed = normalizedPrompt.trim();
448
+ return AFFIRMATIVE_SHORT_REPLIES.some((reply) => trimmed === reply);
449
+ }
450
+
431
451
  function inferIntent(input) {
432
452
  const normalized = input.toLowerCase();
433
453
  if (EXPLORE_KEYWORDS.some((keyword) => normalized.includes(keyword))) {