@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.
- package/README.md +59 -7
- package/README.zh-CN.md +16 -4
- package/package.json +12 -4
- package/src/commands/docs.js +19 -13
- package/src/commands/gate.js +1 -1
- package/src/commands/hook.js +43 -0
- package/src/commands/init.js +83 -8
- package/src/commands/report.js +4 -4
- package/src/commands/state.js +10 -2
- package/src/commands/status.js +169 -11
- package/src/commands/sync.js +88 -0
- package/src/index.js +15 -3
- package/src/lib/claude-hooks.js +49 -0
- package/src/lib/codex-hooks.js +48 -0
- package/src/lib/gemini-hooks.js +76 -0
- package/src/lib/hook-core.js +639 -0
- package/src/lib/hook-io/claude.js +23 -0
- package/src/lib/hook-io/codex.js +23 -0
- package/src/lib/hook-io/gemini.js +130 -0
- package/src/lib/hook-io/shared.js +52 -0
- package/src/lib/host-layout.js +1384 -0
- package/src/lib/output-policy.js +6 -6
- package/src/lib/runtime-paths.js +39 -0
- package/src/lib/task-core.js +104 -20
- package/src/runtime-host/index.js +57 -0
package/src/lib/output-policy.js
CHANGED
|
@@ -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", "
|
|
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, ["
|
|
205
|
+
return hasKeyword(taskContext.goal, ["复用决策", "通用方案", "shared decision", "reusable decision", "通用能力"]);
|
|
206
206
|
}
|
|
207
207
|
if (normalized === "architectural_decision") {
|
|
208
|
-
return hasKeyword(taskContext.goal, ["
|
|
208
|
+
return hasKeyword(taskContext.goal, ["架构决策", "architecture decision", "架构边界", "runtime boundary"]);
|
|
209
209
|
}
|
|
210
210
|
if (normalized === "policy_change") {
|
|
211
|
-
return hasKeyword(taskContext.goal, ["
|
|
211
|
+
return hasKeyword(taskContext.goal, ["治理策略", "policy as code", "policy change", "策略变更"]);
|
|
212
212
|
}
|
|
213
213
|
if (normalized === "protocol_change") {
|
|
214
|
-
return hasKeyword(taskContext.goal, ["
|
|
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
|
|
217
|
+
return hasKeyword(taskContext.goal, ["adapter contract", "host contract", "适配器契约", "宿主契约"]);
|
|
218
218
|
}
|
|
219
219
|
return false;
|
|
220
220
|
}
|
package/src/lib/runtime-paths.js
CHANGED
|
@@ -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
|
+
}
|
package/src/lib/task-core.js
CHANGED
|
@@ -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
|
|
98
|
-
const
|
|
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:
|
|
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
|
-
? `
|
|
328
|
+
? ` 阻断:${taskState.open_questions[0]}`
|
|
288
329
|
: "";
|
|
289
330
|
|
|
290
|
-
return
|
|
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
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
: "
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
+
}
|