@brawnen/agent-harness-cli 0.1.0 → 0.1.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.
@@ -199,22 +199,22 @@ function matchesArtifactCondition(condition, taskContext) {
199
199
  return inferCrossModuleChange(taskContext.scope);
200
200
  }
201
201
  if (normalized === "public_contract_changed") {
202
- return hasKeyword(taskContext.goal, ["api", "schema", "接口", "契约", "协议", "contract"]);
202
+ return hasKeyword(taskContext.goal, ["public contract", "api schema", "schema change", "breaking change", "接口契约", "公开契约", "兼容性变更"]);
203
203
  }
204
204
  if (normalized === "reusable_decision") {
205
- return hasKeyword(taskContext.goal, ["复用", "通用", "shared", "reusable"]);
205
+ return hasKeyword(taskContext.goal, ["复用决策", "通用方案", "shared decision", "reusable decision", "通用能力"]);
206
206
  }
207
207
  if (normalized === "architectural_decision") {
208
- return hasKeyword(taskContext.goal, ["架构", "architecture", "协议", "adapter", "hook"]);
208
+ return hasKeyword(taskContext.goal, ["架构决策", "architecture decision", "架构边界", "runtime boundary"]);
209
209
  }
210
210
  if (normalized === "policy_change") {
211
- return hasKeyword(taskContext.goal, ["策略", "policy", "规则"]);
211
+ return hasKeyword(taskContext.goal, ["治理策略", "policy as code", "policy change", "策略变更"]);
212
212
  }
213
213
  if (normalized === "protocol_change") {
214
- return hasKeyword(taskContext.goal, ["协议", "protocol"]);
214
+ return hasKeyword(taskContext.goal, ["协议变更", "protocol change", "schema migration"]);
215
215
  }
216
216
  if (normalized === "host_adapter_contract_change") {
217
- return hasKeyword(taskContext.goal, ["adapter", "适配", "hook", "codex", "claude", "gemini"]);
217
+ return hasKeyword(taskContext.goal, ["adapter contract", "host contract", "适配器契约", "宿主契约"]);
218
218
  }
219
219
  return false;
220
220
  }
@@ -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,45 @@ 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
+
34
+ const TASK_REFERENCE_KEYWORDS = [
35
+ "刚才那个",
36
+ "刚才这个",
37
+ "刚才的任务",
38
+ "这个任务",
39
+ "这个问题",
40
+ "这个方案",
41
+ "当前任务",
42
+ "前面那个"
43
+ ];
44
+
45
+ const TASK_REPLY_PREFIXES = [
46
+ "先做",
47
+ "先看",
48
+ "先把",
49
+ "先",
50
+ "再",
51
+ "然后",
52
+ "接着",
53
+ "列一下",
54
+ "看一下",
55
+ "看看",
56
+ "全部",
57
+ "只",
58
+ "统一",
59
+ "收敛成",
60
+ "按这个",
61
+ "就按这个",
62
+ "好,",
63
+ "好,",
64
+ "那就"
65
+ ];
66
+
28
67
  const NEW_TASK_KEYWORDS = [
29
68
  "新任务",
30
69
  "另一个问题",
@@ -53,7 +92,6 @@ const MANUAL_CONFIRMATION_KEYWORDS = [
53
92
  "允许继续",
54
93
  "就按这个做",
55
94
  "按这个做",
56
- "继续",
57
95
  "go ahead",
58
96
  "proceed"
59
97
  ];
@@ -94,8 +132,13 @@ export function buildTaskDraftFromInput(sourceInput, options = {}) {
94
132
  const riskLevel = inferRiskLevel(input, riskSignals);
95
133
  const intent = options.intent ?? inferIntent(input);
96
134
  const mode = options.mode ?? (intent === "explore" ? "explore" : "delivery");
97
- const nextAction = openQuestions.length > 0 ? "clarify" : "plan";
98
- const derivedState = openQuestions.length > 0 ? "needs_clarification" : "planned";
135
+ const hasOpenQuestions = openQuestions.length > 0;
136
+ const nextAction = hasOpenQuestions
137
+ ? (riskLevel === "high" ? "clarify" : "observe")
138
+ : "plan";
139
+ const derivedState = hasOpenQuestions
140
+ ? (riskLevel === "high" ? "needs_clarification" : "draft")
141
+ : "planned";
99
142
 
100
143
  return {
101
144
  schema_version: "0.3",
@@ -241,11 +284,11 @@ export function autoIntakePrompt(cwd, prompt) {
241
284
  }
242
285
 
243
286
  const decision = classifyPromptAgainstTask(input, activeTask);
244
- if (decision.type === "continue") {
287
+ if (decision.type === "continue" || decision.type === "provisional_continue") {
245
288
  return {
246
289
  action: "continue",
247
290
  task: activeTask,
248
- additionalContext: buildCurrentTaskContext(activeTask),
291
+ additionalContext: "",
249
292
  decision
250
293
  };
251
294
  }
@@ -281,13 +324,11 @@ export function buildCurrentTaskContext(taskState) {
281
324
  }
282
325
 
283
326
  const goal = taskState?.confirmed_contract?.goal ?? taskState?.task_draft?.goal ?? "未定义目标";
284
- const nextAction = deriveNextAction(taskState);
285
- const currentState = taskState?.current_state ?? "unknown";
286
327
  const blockingQuestion = Array.isArray(taskState?.open_questions) && taskState.open_questions.length > 0
287
- ? ` 当前阻断问题:${taskState.open_questions[0]}`
328
+ ? ` 阻断:${taskState.open_questions[0]}`
288
329
  : "";
289
330
 
290
- return `当前 active task: ${taskState.task_id}。目标:${goal}。当前状态:${currentState}。下一步动作:${nextAction}。${blockingQuestion}`.trim();
331
+ return `当前任务 ${taskState.task_id}:${goal}。${blockingQuestion}`.trim();
291
332
  }
292
333
 
293
334
  export function buildNewTaskContext(taskState) {
@@ -296,7 +337,7 @@ export function buildNewTaskContext(taskState) {
296
337
  }
297
338
 
298
339
  const draft = taskState.task_draft ?? {};
299
- return `已自动创建新任务 ${taskState.task_id}。intent=${draft.intent},goal=${draft.goal},next_action=${draft.next_action}。请先按该任务继续。`;
340
+ return `已切换到新任务 ${taskState.task_id}:${draft.goal}。`;
300
341
  }
301
342
 
302
343
  export function classifyUserOverridePrompt(prompt) {
@@ -389,20 +430,53 @@ function classifyPromptAgainstTask(prompt, activeTask) {
389
430
  });
390
431
  }
391
432
 
433
+ if (isAffirmativeShortReply(normalizedPrompt)) {
434
+ return buildDecision("continue", {
435
+ reasonCode: "affirmative_short_reply",
436
+ reason: "输入为确认性短回复,视为延续当前任务。",
437
+ matchedSignals: ["affirmative_short_reply"],
438
+ confidence: "medium"
439
+ });
440
+ }
441
+
442
+ const matchedTaskReference = TASK_REFERENCE_KEYWORDS.find((keyword) => normalizedPrompt.includes(keyword));
443
+ if (matchedTaskReference) {
444
+ return buildDecision("continue", {
445
+ reasonCode: "matched_task_reference",
446
+ reason: `输入命中当前任务指代:${matchedTaskReference}。`,
447
+ matchedSignals: [`task_reference:${matchedTaskReference}`],
448
+ confidence: "medium"
449
+ });
450
+ }
451
+
452
+ if (isLikelyTaskReply(normalizedPrompt)) {
453
+ return buildDecision("provisional_continue", {
454
+ reasonCode: "likely_task_reply",
455
+ reason: "输入更像是当前任务内的步骤选择或简短回复,先续接当前任务观察。",
456
+ matchedSignals: ["likely_task_reply"],
457
+ confidence: "low"
458
+ });
459
+ }
460
+
392
461
  const ambiguous = isAmbiguousPrompt(prompt);
393
462
  if (ambiguous) {
394
463
  const matchedHighRiskKeyword = findMatchedKeyword(normalizedPrompt, HIGH_RISK_KEYWORDS);
395
464
  const highRisk = Boolean(matchedHighRiskKeyword);
396
- return buildDecision("clarify", {
397
- block: highRisk,
398
- reasonCode: highRisk ? "ambiguous_high_risk_prompt" : "ambiguous_prompt",
399
- reason: highRisk
400
- ? "当前输入任务归属不明且包含高风险信号。先澄清是在延续旧任务还是新任务,再继续执行。"
401
- : "当前输入无法可靠判断是在延续旧任务还是新任务。先向用户澄清任务归属,再继续执行。",
402
- matchedSignals: highRisk
403
- ? [`high_risk_keyword:${matchedHighRiskKeyword}`, "ambiguous_prompt"]
404
- : ["ambiguous_prompt"],
405
- confidence: highRisk ? "high" : "low"
465
+ if (highRisk) {
466
+ return buildDecision("clarify", {
467
+ block: true,
468
+ reasonCode: "ambiguous_high_risk_prompt",
469
+ reason: "输入任务归属不明且含高风险信号,请先澄清。",
470
+ matchedSignals: [`high_risk_keyword:${matchedHighRiskKeyword}`, "ambiguous_prompt"],
471
+ confidence: "high"
472
+ });
473
+ }
474
+
475
+ return buildDecision("provisional_continue", {
476
+ reasonCode: "ambiguous_low_risk_continue",
477
+ reason: "输入较短且无高风险信号,先按当前任务续接并观察。",
478
+ matchedSignals: ["ambiguous_prompt", "low_risk"],
479
+ confidence: "low"
406
480
  });
407
481
  }
408
482
 
@@ -428,6 +502,16 @@ function isAmbiguousPrompt(prompt) {
428
502
  return ["看一下这个", "有个问题", "帮我处理一下", "处理下", "看看这个"].some((text) => normalized.includes(text));
429
503
  }
430
504
 
505
+ function isAffirmativeShortReply(normalizedPrompt) {
506
+ const trimmed = normalizedPrompt.trim();
507
+ return AFFIRMATIVE_SHORT_REPLIES.some((reply) => trimmed === reply);
508
+ }
509
+
510
+ function isLikelyTaskReply(normalizedPrompt) {
511
+ const trimmed = normalizedPrompt.trim();
512
+ return TASK_REPLY_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
513
+ }
514
+
431
515
  function inferIntent(input) {
432
516
  const normalized = input.toLowerCase();
433
517
  if (EXPLORE_KEYWORDS.some((keyword) => normalized.includes(keyword))) {
@@ -0,0 +1,57 @@
1
+ import { getActiveTask, resolveActiveTaskId, updateTaskState } from "../lib/state-store.js";
2
+
3
+ export {
4
+ blockDecision,
5
+ buildManualFallbackContext,
6
+ continueDecision,
7
+ handleAfterTool,
8
+ handleBeforeTool,
9
+ handleCompletionGate,
10
+ handlePromptSubmit,
11
+ handleSessionStart,
12
+ normalizeHarnessToolName
13
+ } from "../lib/hook-core.js";
14
+ export { buildClaudeHookOutput, resolveClaudeCompletionMessage } from "../lib/hook-io/claude.js";
15
+ export { buildCodexHookOutput, resolveCodexCompletionMessage } from "../lib/hook-io/codex.js";
16
+ export {
17
+ buildGeminiHookOutput,
18
+ resolveGeminiCompletionMessage,
19
+ resolveGeminiToolCommand,
20
+ resolveGeminiToolExitCode,
21
+ resolveGeminiToolName,
22
+ resolveGeminiToolOutput,
23
+ resolveGeminiToolPath
24
+ } from "../lib/hook-io/gemini.js";
25
+
26
+ export function appendMinimalToolEvidence({
27
+ content = null,
28
+ cwd,
29
+ exitCode = 0,
30
+ toolName = null,
31
+ type = "command_result"
32
+ }) {
33
+ const taskId = resolveActiveTaskId(cwd);
34
+ const activeTask = getActiveTask(cwd);
35
+
36
+ if (!taskId || !activeTask || ["done", "failed", "suspended"].includes(activeTask.current_state)) {
37
+ return false;
38
+ }
39
+
40
+ const safeToolName = typeof toolName === "string" && toolName.trim().length > 0
41
+ ? toolName.trim()
42
+ : "<unknown tool>";
43
+ const evidenceContent = typeof content === "string" && content.trim().length > 0
44
+ ? content.trim()
45
+ : `Tool: ${safeToolName}`;
46
+
47
+ updateTaskState(cwd, taskId, {
48
+ evidence: [{
49
+ content: evidenceContent,
50
+ exit_code: typeof exitCode === "number" ? exitCode : 0,
51
+ timestamp: new Date().toISOString(),
52
+ type
53
+ }]
54
+ });
55
+
56
+ return true;
57
+ }