@cjhyy/code-shell-core 0.5.0-rc.0 → 0.5.0-rc.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/dist/agent/agent-definition-registry.d.ts +29 -0
- package/dist/agent/agent-definition-registry.js +74 -0
- package/dist/agent/agent-definition.d.ts +37 -0
- package/dist/agent/agent-definition.js +59 -0
- package/dist/arena/arena.js +11 -12
- package/dist/arena/context/context-tools.js +2 -4
- package/dist/arena/context/within-root.d.ts +7 -0
- package/dist/arena/context/within-root.js +15 -0
- package/dist/arena/iterate/phases/argue.js +8 -2
- package/dist/arena/iterate/phases/revise.js +1 -1
- package/dist/arena/iterate/phases/tournament.js +3 -3
- package/dist/arena/phases/adjudication.js +1 -4
- package/dist/arena/phases/build-consensus.js +1 -4
- package/dist/arena/phases/cross-review.js +2 -8
- package/dist/arena/phases/debate-rounds.js +1 -4
- package/dist/arena/phases/participant-research.js +1 -4
- package/dist/arena/phases/planning-detail-expansion.js +8 -10
- package/dist/arena/planner.js +0 -1
- package/dist/arena/providers/docs.d.ts +1 -0
- package/dist/arena/providers/docs.js +5 -2
- package/dist/arena/providers/repo.d.ts +1 -0
- package/dist/arena/providers/repo.js +5 -2
- package/dist/arena/strategies/utils.js +36 -3
- package/dist/arena/transitions.d.ts +7 -2
- package/dist/arena/transitions.js +9 -4
- package/dist/arena/types.d.ts +8 -2
- package/dist/automation/cron-expr.d.ts +31 -0
- package/dist/automation/cron-expr.js +151 -0
- package/dist/automation/index.d.ts +41 -0
- package/dist/automation/index.js +39 -0
- package/dist/automation/runner.d.ts +67 -0
- package/dist/automation/runner.js +56 -0
- package/dist/automation/scheduler.d.ts +156 -0
- package/dist/automation/scheduler.js +546 -0
- package/dist/automation/store.d.ts +39 -0
- package/dist/automation/store.js +119 -0
- package/dist/automation/write-policy.d.ts +34 -0
- package/dist/automation/write-policy.js +113 -0
- package/dist/automation/write-run.d.ts +51 -0
- package/dist/automation/write-run.js +38 -0
- package/dist/capability-control/index.d.ts +5 -0
- package/dist/capability-control/index.js +3 -0
- package/dist/capability-control/overlay.d.ts +49 -0
- package/dist/capability-control/overlay.js +89 -0
- package/dist/capability-control/project.d.ts +65 -0
- package/dist/capability-control/project.js +147 -0
- package/dist/capability-control/service.d.ts +70 -0
- package/dist/capability-control/service.js +147 -0
- package/dist/capability-control/types.d.ts +62 -0
- package/dist/capability-control/types.js +17 -0
- package/dist/cli/agent-server-stdio.d.ts +12 -1
- package/dist/cli/agent-server-stdio.js +118 -18
- package/dist/cli/agent-server-tcp.d.ts +20 -0
- package/dist/cli/agent-server-tcp.js +120 -0
- package/dist/cli/graceful-shutdown.d.ts +27 -0
- package/dist/cli/graceful-shutdown.js +30 -0
- package/dist/context/compaction.js +23 -15
- package/dist/context/manager.d.ts +0 -20
- package/dist/context/manager.js +0 -37
- package/dist/cron/cron-runtime.d.ts +2 -0
- package/dist/cron/cron-runtime.js +2 -0
- package/dist/cron/cron-store.d.ts +2 -0
- package/dist/cron/cron-store.js +2 -0
- package/dist/cron/scheduler.d.ts +5 -32
- package/dist/cron/scheduler.js +5 -113
- package/dist/data/openrouter-sync.js +1 -1
- package/dist/engine/engine.d.ts +256 -16
- package/dist/engine/engine.js +856 -200
- package/dist/engine/goal.d.ts +42 -0
- package/dist/engine/goal.js +52 -0
- package/dist/engine/image-compression.d.ts +81 -0
- package/dist/engine/image-compression.js +186 -0
- package/dist/engine/image-policy.d.ts +152 -0
- package/dist/engine/image-policy.js +165 -0
- package/dist/engine/reactive-threshold.d.ts +13 -0
- package/dist/engine/reactive-threshold.js +17 -0
- package/dist/engine/runtime.js +7 -0
- package/dist/engine/session-title.d.ts +13 -0
- package/dist/engine/session-title.js +34 -0
- package/dist/engine/turn-loop.d.ts +29 -8
- package/dist/engine/turn-loop.js +498 -316
- package/dist/git/parse-log.d.ts +13 -0
- package/dist/git/parse-log.js +21 -0
- package/dist/git/utils.d.ts +8 -6
- package/dist/git/utils.js +57 -26
- package/dist/git/worktree.js +33 -18
- package/dist/hooks/events.d.ts +23 -1
- package/dist/hooks/goal-stop-hook.d.ts +30 -0
- package/dist/hooks/goal-stop-hook.js +102 -0
- package/dist/hooks/hook-output.d.ts +33 -0
- package/dist/hooks/hook-output.js +79 -0
- package/dist/hooks/registry.d.ts +7 -0
- package/dist/hooks/registry.js +38 -1
- package/dist/hooks/shell-runner.d.ts +2 -0
- package/dist/hooks/shell-runner.js +43 -2
- package/dist/index.d.ts +32 -7
- package/dist/index.js +30 -3
- package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
- package/dist/llm/capabilities/reasoning-control.js +38 -0
- package/dist/llm/capabilities/rules.js +33 -3
- package/dist/llm/capabilities/types.d.ts +22 -2
- package/dist/llm/clamp-max-tokens.d.ts +11 -0
- package/dist/llm/clamp-max-tokens.js +17 -0
- package/dist/llm/client-base.d.ts +41 -4
- package/dist/llm/client-base.js +67 -13
- package/dist/llm/client-factory.d.ts +3 -3
- package/dist/llm/client-factory.js +2 -2
- package/dist/llm/model-pool.d.ts +15 -11
- package/dist/llm/model-pool.js +23 -20
- package/dist/llm/provider-catalog.d.ts +2 -2
- package/dist/llm/providers/anthropic.d.ts +32 -2
- package/dist/llm/providers/anthropic.js +147 -8
- package/dist/llm/providers/openai.d.ts +11 -2
- package/dist/llm/providers/openai.js +253 -52
- package/dist/llm/reasoning-setting.d.ts +51 -0
- package/dist/llm/reasoning-setting.js +32 -0
- package/dist/llm/stop-reason.d.ts +13 -0
- package/dist/llm/stop-reason.js +21 -0
- package/dist/llm/strip-vision.d.ts +32 -0
- package/dist/llm/strip-vision.js +94 -0
- package/dist/llm/types.d.ts +5 -6
- package/dist/logging/logger.js +18 -4
- package/dist/logging/sanitize-messages.d.ts +10 -0
- package/dist/logging/sanitize-messages.js +90 -0
- package/dist/lsp/client.js +28 -8
- package/dist/lsp/manager.js +2 -1
- package/dist/lsp/root-path.d.ts +9 -0
- package/dist/lsp/root-path.js +12 -0
- package/dist/onboarding.js +18 -11
- package/dist/plugins/gitOps.d.ts +34 -0
- package/dist/plugins/gitOps.js +59 -3
- package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
- package/dist/plugins/installer/codex/convertAgents.js +55 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
- package/dist/plugins/installer/codex/convertMcp.js +45 -0
- package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
- package/dist/plugins/installer/codex/convertSkills.js +33 -0
- package/dist/plugins/installer/detectFormat.d.ts +2 -0
- package/dist/plugins/installer/detectFormat.js +6 -0
- package/dist/plugins/installer/install.d.ts +7 -0
- package/dist/plugins/installer/install.js +99 -0
- package/dist/plugins/installer/installFromSource.d.ts +13 -0
- package/dist/plugins/installer/installFromSource.js +45 -0
- package/dist/plugins/installer/list.d.ts +9 -0
- package/dist/plugins/installer/list.js +30 -0
- package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
- package/dist/plugins/installer/loadPluginAgents.js +23 -0
- package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
- package/dist/plugins/installer/loadPluginMcp.js +74 -0
- package/dist/plugins/installer/parseSource.d.ts +20 -0
- package/dist/plugins/installer/parseSource.js +74 -0
- package/dist/plugins/installer/paths.d.ts +5 -0
- package/dist/plugins/installer/paths.js +25 -0
- package/dist/plugins/installer/types.d.ts +49 -0
- package/dist/plugins/installer/types.js +27 -0
- package/dist/plugins/installer/uninstall.d.ts +2 -0
- package/dist/plugins/installer/uninstall.js +14 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +65 -0
- package/dist/plugins/loadPluginHooks.d.ts +8 -1
- package/dist/plugins/loadPluginHooks.js +11 -1
- package/dist/plugins/marketplaceManager.d.ts +8 -1
- package/dist/plugins/marketplaceManager.js +38 -4
- package/dist/plugins/pluginCommandHook.js +34 -0
- package/dist/plugins/pluginInstaller.d.ts +21 -0
- package/dist/plugins/pluginInstaller.js +128 -29
- package/dist/plugins/schemas.js +40 -6
- package/dist/plugins/types.d.ts +4 -0
- package/dist/preset/index.js +16 -0
- package/dist/prompt/composer.d.ts +10 -0
- package/dist/prompt/composer.js +29 -4
- package/dist/prompt/sections/orchestration.md +8 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -1
- package/dist/protocol/chat-session-manager.js +10 -0
- package/dist/protocol/chat-session.d.ts +17 -0
- package/dist/protocol/chat-session.js +28 -0
- package/dist/protocol/redact.d.ts +50 -0
- package/dist/protocol/redact.js +71 -0
- package/dist/protocol/server.d.ts +41 -2
- package/dist/protocol/server.js +231 -33
- package/dist/protocol/tcp-transport.d.ts +45 -0
- package/dist/protocol/tcp-transport.js +74 -0
- package/dist/protocol/types.d.ts +23 -0
- package/dist/protocol/types.js +6 -0
- package/dist/remote/bridge.d.ts +11 -2
- package/dist/remote/bridge.js +61 -33
- package/dist/run/ArtifactTracker.js +5 -4
- package/dist/run/EngineRunner.d.ts +27 -1
- package/dist/run/EngineRunner.js +46 -8
- package/dist/run/FileRunStore.js +32 -8
- package/dist/run/RunLock.d.ts +28 -2
- package/dist/run/RunLock.js +49 -7
- package/dist/run/RunManager.js +51 -3
- package/dist/run/RunQueue.d.ts +1 -0
- package/dist/run/RunQueue.js +8 -2
- package/dist/run/factory.d.ts +7 -0
- package/dist/run/factory.js +1 -0
- package/dist/run/index.d.ts +2 -2
- package/dist/run/index.js +1 -1
- package/dist/run/redirect-target.d.ts +7 -0
- package/dist/run/redirect-target.js +13 -0
- package/dist/run/types.js +1 -1
- package/dist/services/browser-open.d.ts +13 -0
- package/dist/services/browser-open.js +16 -0
- package/dist/services/dream-consolidation.d.ts +57 -0
- package/dist/services/dream-consolidation.js +151 -0
- package/dist/services/memory-orchestrator.js +18 -0
- package/dist/services/notifier.d.ts +8 -0
- package/dist/services/notifier.js +39 -25
- package/dist/services/oauth.js +5 -7
- package/dist/services/session-memory-sort.d.ts +8 -0
- package/dist/services/session-memory-sort.js +9 -0
- package/dist/services/session-memory.js +8 -9
- package/dist/session/file-history.js +10 -3
- package/dist/session/session-manager.d.ts +23 -1
- package/dist/session/session-manager.js +73 -2
- package/dist/settings/disk-defaults.d.ts +35 -0
- package/dist/settings/disk-defaults.js +24 -0
- package/dist/settings/manager.d.ts +45 -1
- package/dist/settings/manager.js +121 -17
- package/dist/settings/manager.test.d.ts +1 -0
- package/dist/settings/manager.test.js +73 -0
- package/dist/settings/personalization.d.ts +19 -0
- package/dist/settings/personalization.js +7 -0
- package/dist/settings/schema.d.ts +1163 -126
- package/dist/settings/schema.js +126 -10
- package/dist/skills/scanner.d.ts +13 -6
- package/dist/skills/scanner.js +23 -4
- package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
- package/dist/tool-system/builtin/add-marketplace.js +76 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +15 -0
- package/dist/tool-system/builtin/agent-registry.js +14 -4
- package/dist/tool-system/builtin/agent.d.ts +55 -0
- package/dist/tool-system/builtin/agent.js +165 -17
- package/dist/tool-system/builtin/apply-patch/index.js +22 -3
- package/dist/tool-system/builtin/arena.js +6 -7
- package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
- package/dist/tool-system/builtin/complete-goal.js +45 -0
- package/dist/tool-system/builtin/cron.js +53 -9
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +6 -1
- package/dist/tool-system/builtin/generate-image.d.ts +25 -0
- package/dist/tool-system/builtin/generate-image.js +146 -0
- package/dist/tool-system/builtin/glob.js +7 -0
- package/dist/tool-system/builtin/grep.js +6 -0
- package/dist/tool-system/builtin/index.d.ts +18 -1
- package/dist/tool-system/builtin/index.js +62 -1
- package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
- package/dist/tool-system/builtin/notebook-edit.js +8 -1
- package/dist/tool-system/builtin/read.d.ts +2 -1
- package/dist/tool-system/builtin/read.js +9 -1
- package/dist/tool-system/builtin/skill.js +23 -5
- package/dist/tool-system/builtin/sleep.js +9 -3
- package/dist/tool-system/builtin/task.js +4 -1
- package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
- package/dist/tool-system/builtin/update-automation-memory.js +60 -0
- package/dist/tool-system/builtin/view-image.d.ts +18 -0
- package/dist/tool-system/builtin/view-image.js +96 -0
- package/dist/tool-system/builtin/web-search.d.ts +4 -1
- package/dist/tool-system/builtin/web-search.js +36 -3
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +7 -1
- package/dist/tool-system/context.d.ts +43 -0
- package/dist/tool-system/executor.d.ts +0 -1
- package/dist/tool-system/executor.js +34 -40
- package/dist/tool-system/investigation-guard.d.ts +4 -1
- package/dist/tool-system/investigation-guard.js +19 -3
- package/dist/tool-system/mcp-manager.d.ts +36 -12
- package/dist/tool-system/mcp-manager.js +173 -10
- package/dist/tool-system/path-policy.d.ts +94 -0
- package/dist/tool-system/path-policy.js +279 -0
- package/dist/tool-system/permission.js +43 -50
- package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
- package/dist/tool-system/plan-mode-allowlist.js +43 -0
- package/dist/tool-system/registry.js +12 -1
- package/dist/tool-system/validation.d.ts +10 -0
- package/dist/tool-system/validation.js +16 -3
- package/dist/types.d.ts +90 -15
- package/dist/utils/format.js +4 -2
- package/dist/utils/lockfile.js +9 -2
- package/dist/utils/theme.d.ts +6 -0
- package/dist/utils/theme.js +4 -2
- package/package.json +2 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface GitLogEntry {
|
|
2
|
+
hash: string;
|
|
3
|
+
message: string;
|
|
4
|
+
author: string;
|
|
5
|
+
date: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse `git log --format=%H|%s|%an|%ci` output into entries. Tolerates
|
|
9
|
+
* malformed/short lines (missing `|` separators) by defaulting absent fields
|
|
10
|
+
* to "" rather than destructuring `undefined` and crashing on `.slice`
|
|
11
|
+
* (review-2026-05-30). Blank lines are skipped.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseGitLog(raw: string): GitLogEntry[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse `git log --format=%H|%s|%an|%ci` output into entries. Tolerates
|
|
3
|
+
* malformed/short lines (missing `|` separators) by defaulting absent fields
|
|
4
|
+
* to "" rather than destructuring `undefined` and crashing on `.slice`
|
|
5
|
+
* (review-2026-05-30). Blank lines are skipped.
|
|
6
|
+
*/
|
|
7
|
+
export function parseGitLog(raw) {
|
|
8
|
+
return raw
|
|
9
|
+
.split("\n")
|
|
10
|
+
.filter((line) => line.trim() !== "")
|
|
11
|
+
.map((line) => {
|
|
12
|
+
const parts = line.split("|");
|
|
13
|
+
const hash = parts[0] ?? "";
|
|
14
|
+
return {
|
|
15
|
+
hash: hash.slice(0, 8),
|
|
16
|
+
message: parts[1] ?? "",
|
|
17
|
+
author: parts[2] ?? "",
|
|
18
|
+
date: parts[3] ?? "",
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
}
|
package/dist/git/utils.d.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Git utility functions for the git workflow commands.
|
|
3
|
+
*
|
|
4
|
+
* Every call goes through execFileSync with an argv array — no command
|
|
5
|
+
* strings, no shell interpolation. Even arguments that look "safe" (already
|
|
6
|
+
* quoted, hard-coded) are passed as separate argv tokens so a future caller
|
|
7
|
+
* can't accidentally widen the attack surface by interpolating user input
|
|
8
|
+
* into the string form.
|
|
3
9
|
*/
|
|
10
|
+
import { type GitLogEntry } from "./parse-log.js";
|
|
4
11
|
export interface GitStatusEntry {
|
|
5
12
|
status: string;
|
|
6
13
|
path: string;
|
|
7
14
|
}
|
|
8
|
-
export
|
|
9
|
-
hash: string;
|
|
10
|
-
message: string;
|
|
11
|
-
author: string;
|
|
12
|
-
date: string;
|
|
13
|
-
}
|
|
15
|
+
export type { GitLogEntry };
|
|
14
16
|
export declare function isGitRepo(cwd: string): boolean;
|
|
15
17
|
export declare function getCurrentBranch(cwd: string): string;
|
|
16
18
|
export declare function getGitStatus(cwd: string): GitStatusEntry[];
|
package/dist/git/utils.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Git utility functions for the git workflow commands.
|
|
3
|
+
*
|
|
4
|
+
* Every call goes through execFileSync with an argv array — no command
|
|
5
|
+
* strings, no shell interpolation. Even arguments that look "safe" (already
|
|
6
|
+
* quoted, hard-coded) are passed as separate argv tokens so a future caller
|
|
7
|
+
* can't accidentally widen the attack surface by interpolating user input
|
|
8
|
+
* into the string form.
|
|
3
9
|
*/
|
|
4
|
-
import {
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { parseGitLog } from "./parse-log.js";
|
|
12
|
+
/** Run git with an argv array and return its trimmed stdout. */
|
|
13
|
+
function git(cwd, args, timeoutMs = 10000) {
|
|
14
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", timeout: timeoutMs }).trim();
|
|
15
|
+
}
|
|
16
|
+
/** Run gh with an argv array and return its trimmed stdout. */
|
|
17
|
+
function gh(cwd, args, timeoutMs = 10000) {
|
|
18
|
+
return execFileSync("gh", args, { cwd, encoding: "utf-8", timeout: timeoutMs }).trim();
|
|
19
|
+
}
|
|
5
20
|
export function isGitRepo(cwd) {
|
|
6
21
|
try {
|
|
7
|
-
|
|
22
|
+
git(cwd, ["rev-parse", "--is-inside-work-tree"], 5000);
|
|
8
23
|
return true;
|
|
9
24
|
}
|
|
10
25
|
catch {
|
|
@@ -12,10 +27,10 @@ export function isGitRepo(cwd) {
|
|
|
12
27
|
}
|
|
13
28
|
}
|
|
14
29
|
export function getCurrentBranch(cwd) {
|
|
15
|
-
return
|
|
30
|
+
return git(cwd, ["branch", "--show-current"], 5000);
|
|
16
31
|
}
|
|
17
32
|
export function getGitStatus(cwd) {
|
|
18
|
-
const raw =
|
|
33
|
+
const raw = git(cwd, ["status", "--porcelain"], 10000);
|
|
19
34
|
if (!raw)
|
|
20
35
|
return [];
|
|
21
36
|
return raw.split("\n").map((line) => ({
|
|
@@ -24,51 +39,58 @@ export function getGitStatus(cwd) {
|
|
|
24
39
|
}));
|
|
25
40
|
}
|
|
26
41
|
export function getGitDiff(cwd, opts) {
|
|
27
|
-
const args = ["
|
|
42
|
+
const args = ["diff", "--no-color"];
|
|
28
43
|
if (opts?.staged)
|
|
29
44
|
args.push("--staged");
|
|
45
|
+
// `--` terminates option parsing so a file path starting with `-` can't
|
|
46
|
+
// be re-interpreted as a flag.
|
|
30
47
|
if (opts?.file)
|
|
31
48
|
args.push("--", opts.file);
|
|
32
|
-
return
|
|
49
|
+
return git(cwd, args, 30000);
|
|
33
50
|
}
|
|
34
51
|
export function getGitDiffStat(cwd, opts) {
|
|
35
|
-
const args = ["
|
|
52
|
+
const args = ["diff", "--stat", "--no-color"];
|
|
36
53
|
if (opts?.staged)
|
|
37
54
|
args.push("--staged");
|
|
38
55
|
if (opts?.file)
|
|
39
56
|
args.push("--", opts.file);
|
|
40
|
-
return
|
|
57
|
+
return git(cwd, args, 10000);
|
|
41
58
|
}
|
|
42
59
|
export function getGitLog(cwd, n = 10) {
|
|
43
|
-
|
|
60
|
+
// `n` is numeric — coerce/validate to keep the argv clean even if a caller
|
|
61
|
+
// hands us a string.
|
|
62
|
+
const count = Number.isFinite(n) && n > 0 ? Math.floor(n) : 10;
|
|
63
|
+
const raw = git(cwd, ["log", "--oneline", "--format=%H|%s|%an|%ci", `-${count}`], 10000);
|
|
44
64
|
if (!raw)
|
|
45
65
|
return [];
|
|
46
|
-
return raw
|
|
47
|
-
const [hash, message, author, date] = line.split("|");
|
|
48
|
-
return { hash: hash.slice(0, 8), message, author, date };
|
|
49
|
-
});
|
|
66
|
+
return parseGitLog(raw);
|
|
50
67
|
}
|
|
51
68
|
export function getRemoteUrl(cwd) {
|
|
52
69
|
try {
|
|
53
|
-
return
|
|
70
|
+
return git(cwd, ["remote", "get-url", "origin"], 5000);
|
|
54
71
|
}
|
|
55
72
|
catch {
|
|
56
73
|
return undefined;
|
|
57
74
|
}
|
|
58
75
|
}
|
|
59
76
|
export function gitAdd(cwd, files = ["."]) {
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
// `--` ensures a path starting with `-` cannot be parsed as a flag.
|
|
78
|
+
// Each file is its own argv token, so spaces / quotes / non-ASCII pass
|
|
79
|
+
// through verbatim with no shell parsing.
|
|
80
|
+
execFileSync("git", ["add", "--", ...files], { cwd, timeout: 10000 });
|
|
62
81
|
}
|
|
63
82
|
export function gitCommit(cwd, message) {
|
|
64
|
-
|
|
83
|
+
// Pre-fix this used `JSON.stringify(message)` which only happened to be
|
|
84
|
+
// safe because JSON.stringify covers most shell metacharacters — but it's
|
|
85
|
+
// not real escaping. The argv form is.
|
|
86
|
+
return execFileSync("git", ["commit", "-m", message], {
|
|
65
87
|
cwd,
|
|
66
88
|
encoding: "utf-8",
|
|
67
89
|
timeout: 30000,
|
|
68
90
|
}).trim();
|
|
69
91
|
}
|
|
70
92
|
export function gitListBranches(cwd) {
|
|
71
|
-
const raw =
|
|
93
|
+
const raw = git(cwd, ["branch", "--no-color"], 5000);
|
|
72
94
|
if (!raw)
|
|
73
95
|
return [];
|
|
74
96
|
return raw.split("\n").map((line) => ({
|
|
@@ -77,12 +99,22 @@ export function gitListBranches(cwd) {
|
|
|
77
99
|
}));
|
|
78
100
|
}
|
|
79
101
|
export function gitCheckout(cwd, branch, create = false) {
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
// argv form defeats shell injection. We additionally reject branch names
|
|
103
|
+
// that start with `-` so a value like `--orphan` can't slip through git's
|
|
104
|
+
// own option parser (the trailing `--` trick doesn't help on checkout:
|
|
105
|
+
// git interprets it as the option's value, not as an option terminator).
|
|
106
|
+
if (typeof branch !== "string" || branch.length === 0) {
|
|
107
|
+
throw new Error("branch must be a non-empty string");
|
|
108
|
+
}
|
|
109
|
+
if (branch.startsWith("-")) {
|
|
110
|
+
throw new Error(`refusing branch name that starts with '-': ${branch}`);
|
|
111
|
+
}
|
|
112
|
+
const args = create ? ["checkout", "-b", branch] : ["checkout", branch];
|
|
113
|
+
execFileSync("git", args, { cwd, timeout: 10000 });
|
|
82
114
|
}
|
|
83
115
|
export function ghAvailable() {
|
|
84
116
|
try {
|
|
85
|
-
|
|
117
|
+
execFileSync("gh", ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
86
118
|
return true;
|
|
87
119
|
}
|
|
88
120
|
catch {
|
|
@@ -90,9 +122,8 @@ export function ghAvailable() {
|
|
|
90
122
|
}
|
|
91
123
|
}
|
|
92
124
|
export function ghPrComments(cwd, prUrl) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}).trim();
|
|
125
|
+
// Pre-fix `gh pr view ${prUrl}` interpolated the URL directly. argv form
|
|
126
|
+
// means even a value like "$(rm -rf ~)" is sent to gh as a literal
|
|
127
|
+
// positional argument (which gh will then reject as a bad PR URL).
|
|
128
|
+
return gh(cwd, ["pr", "view", prUrl, "--comments", "--json", "comments", "-q", ".comments[].body"], 30000);
|
|
98
129
|
}
|
package/dist/git/worktree.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Git worktree management — create/remove isolated worktrees for agents.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { existsSync, symlinkSync, lstatSync } from "node:fs";
|
|
6
|
-
import { join, resolve } from "node:path";
|
|
6
|
+
import { join, resolve, dirname } from "node:path";
|
|
7
7
|
/**
|
|
8
8
|
* Validate worktree slug to prevent path traversal attacks.
|
|
9
9
|
*/
|
|
@@ -19,7 +19,7 @@ export function validateWorktreeSlug(slug) {
|
|
|
19
19
|
* Find the canonical git root (resolves worktree → main repo).
|
|
20
20
|
*/
|
|
21
21
|
export function findGitRoot(cwd) {
|
|
22
|
-
return
|
|
22
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8", timeout: 5000 }).trim();
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Create an isolated git worktree for an agent session.
|
|
@@ -32,7 +32,7 @@ export function createWorktree(cwd, slug, sessionId) {
|
|
|
32
32
|
// Get current branch for later reference
|
|
33
33
|
let originalBranch;
|
|
34
34
|
try {
|
|
35
|
-
originalBranch =
|
|
35
|
+
originalBranch = execFileSync("git", ["branch", "--show-current"], {
|
|
36
36
|
cwd: gitRoot,
|
|
37
37
|
encoding: "utf-8",
|
|
38
38
|
timeout: 5000,
|
|
@@ -41,8 +41,10 @@ export function createWorktree(cwd, slug, sessionId) {
|
|
|
41
41
|
catch {
|
|
42
42
|
// Detached HEAD
|
|
43
43
|
}
|
|
44
|
-
// Create the worktree
|
|
45
|
-
|
|
44
|
+
// Create the worktree. Pre-fix this used a quoted command string; the argv
|
|
45
|
+
// form keeps branchName/worktreePath as literal positional arguments even
|
|
46
|
+
// if a future caller bypasses validateWorktreeSlug.
|
|
47
|
+
execFileSync("git", ["worktree", "add", "-b", branchName, worktreePath], {
|
|
46
48
|
cwd: gitRoot,
|
|
47
49
|
timeout: 30000,
|
|
48
50
|
});
|
|
@@ -63,25 +65,38 @@ export function createWorktree(cwd, slug, sessionId) {
|
|
|
63
65
|
*/
|
|
64
66
|
export function removeWorktree(worktreePath, removeBranch = false) {
|
|
65
67
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
68
|
+
// The MAIN repo root, not the worktree's own toplevel. `git rev-parse
|
|
69
|
+
// --show-toplevel` from inside a worktree returns the worktree path, which
|
|
70
|
+
// is about to be deleted; the branch-delete must run from the main repo,
|
|
71
|
+
// which outlives the worktree. Derive it from the common git dir.
|
|
72
|
+
const commonDir = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: worktreePath, encoding: "utf-8", timeout: 5000 }).trim();
|
|
73
|
+
const mainRoot = dirname(commonDir); // <main>/.git → <main>
|
|
74
|
+
// Capture the worktree's branch BEFORE removing the worktree — afterwards
|
|
75
|
+
// the directory is gone and `git branch --show-current` in it would fail
|
|
76
|
+
// (silently, leaving removeBranch a no-op).
|
|
77
|
+
let branch = "";
|
|
71
78
|
if (removeBranch) {
|
|
72
|
-
// Extract branch name from worktree
|
|
73
79
|
try {
|
|
74
|
-
|
|
80
|
+
branch = execFileSync("git", ["branch", "--show-current"], {
|
|
75
81
|
cwd: worktreePath,
|
|
76
82
|
encoding: "utf-8",
|
|
77
83
|
timeout: 5000,
|
|
78
84
|
}).trim();
|
|
79
|
-
if (branch.startsWith("worktree/")) {
|
|
80
|
-
execSync(`git branch -D "${branch}"`, { cwd: gitRoot, timeout: 10000 });
|
|
81
|
-
}
|
|
82
85
|
}
|
|
83
86
|
catch {
|
|
84
|
-
//
|
|
87
|
+
// Detached HEAD or unreadable — nothing to delete.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
execFileSync("git", ["worktree", "remove", worktreePath, "--force"], {
|
|
91
|
+
cwd: mainRoot,
|
|
92
|
+
timeout: 30000,
|
|
93
|
+
});
|
|
94
|
+
if (removeBranch && branch.startsWith("worktree/")) {
|
|
95
|
+
try {
|
|
96
|
+
execFileSync("git", ["branch", "-D", branch], { cwd: mainRoot, timeout: 10000 });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Branch might already be removed.
|
|
85
100
|
}
|
|
86
101
|
}
|
|
87
102
|
}
|
|
@@ -93,7 +108,7 @@ export function removeWorktree(worktreePath, removeBranch = false) {
|
|
|
93
108
|
* List active worktrees.
|
|
94
109
|
*/
|
|
95
110
|
export function listWorktrees(cwd) {
|
|
96
|
-
const raw =
|
|
111
|
+
const raw = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
97
112
|
cwd,
|
|
98
113
|
encoding: "utf-8",
|
|
99
114
|
timeout: 10000,
|
package/dist/hooks/events.d.ts
CHANGED
|
@@ -23,6 +23,21 @@
|
|
|
23
23
|
* may return `messages` to inject a
|
|
24
24
|
* per-turn reminder appended to the
|
|
25
25
|
* conversation right before the model call.
|
|
26
|
+
* - on_stop (turn-loop.ts) — fires when the model
|
|
27
|
+
* returns no tool calls and the loop is
|
|
28
|
+
* about to return reason "completed". A
|
|
29
|
+
* handler returning `continueSession: true`
|
|
30
|
+
* (with `messages`) BLOCKS termination:
|
|
31
|
+
* the messages are injected as a
|
|
32
|
+
* <system-reminder> and the loop runs
|
|
33
|
+
* another turn instead of stopping. This
|
|
34
|
+
* is the seam Goal mode uses (see
|
|
35
|
+
* goal-stop-hook.ts). Bounded by a
|
|
36
|
+
* consecutive-block cap (maxStopBlocks)
|
|
37
|
+
* and maxTurns. ctx.data carries `goal`,
|
|
38
|
+
* `finalText`, `turnCount`. Distinct from
|
|
39
|
+
* HookResult.stop, which controls the hook
|
|
40
|
+
* CHAIN, not agent termination.
|
|
26
41
|
* - pre_tool_use / post_tool_use (executor.ts) — pre_tool_use honors
|
|
27
42
|
* `decision: "deny"` to short-circuit
|
|
28
43
|
* the call (executor.ts:131).
|
|
@@ -72,7 +87,7 @@
|
|
|
72
87
|
* ContextManager; current implementation only knows
|
|
73
88
|
* after-the-fact (use post_compact instead).
|
|
74
89
|
*/
|
|
75
|
-
export type HookEventName = "on_agent_start" | "on_agent_end" | "on_turn_start" | "on_turn_end" | "on_tool_start" | "on_tool_end" | "on_permission_check" | "on_session_start" | "on_session_end" | "pre_tool_use" | "post_tool_use" | "user_prompt_submit" | "pre_compact" | "post_compact" | "file_changed" | "notification";
|
|
90
|
+
export type HookEventName = "on_agent_start" | "on_agent_end" | "on_turn_start" | "on_turn_end" | "on_stop" | "on_tool_start" | "on_tool_end" | "on_permission_check" | "on_session_start" | "on_session_end" | "pre_tool_use" | "post_tool_use" | "user_prompt_submit" | "pre_compact" | "post_compact" | "file_changed" | "notification";
|
|
76
91
|
export interface HookContext {
|
|
77
92
|
eventName: HookEventName;
|
|
78
93
|
data: Record<string, unknown>;
|
|
@@ -82,6 +97,13 @@ export interface HookContext {
|
|
|
82
97
|
export interface HookResult {
|
|
83
98
|
/** If true, stop the hook chain */
|
|
84
99
|
stop?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* For on_stop: if true, BLOCK agent termination — the loop injects this
|
|
102
|
+
* result's `messages` and runs another turn instead of returning
|
|
103
|
+
* "completed". Distinct from `stop` (which only short-circuits the hook
|
|
104
|
+
* chain). Ignored for non-on_stop events. Bounded by maxStopBlocks.
|
|
105
|
+
*/
|
|
106
|
+
continueSession?: boolean;
|
|
85
107
|
/** Modified data to pass along */
|
|
86
108
|
data?: Record<string, unknown>;
|
|
87
109
|
/** Additional messages to inject */
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HookHandler } from "./registry.js";
|
|
2
|
+
import type { LLMResponse } from "../types.js";
|
|
3
|
+
import { type GoalConfig } from "../engine/goal.js";
|
|
4
|
+
/** Narrow LLM surface the judge needs — just a one-shot completion. */
|
|
5
|
+
export interface GoalJudgeLLM {
|
|
6
|
+
createMessage(options: {
|
|
7
|
+
systemPrompt: string;
|
|
8
|
+
messages: {
|
|
9
|
+
role: "user" | "assistant" | "system";
|
|
10
|
+
content: string;
|
|
11
|
+
}[];
|
|
12
|
+
stream?: boolean;
|
|
13
|
+
maxTokens?: number;
|
|
14
|
+
recordUsage?: boolean;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}): Promise<LLMResponse>;
|
|
17
|
+
}
|
|
18
|
+
interface GoalLogger {
|
|
19
|
+
info: (msg: string, data?: Record<string, unknown>) => void;
|
|
20
|
+
warn: (msg: string, data?: Record<string, unknown>) => void;
|
|
21
|
+
error: (msg: string, data?: Record<string, unknown>) => void;
|
|
22
|
+
}
|
|
23
|
+
export interface GoalStopHookOptions {
|
|
24
|
+
llm: GoalJudgeLLM;
|
|
25
|
+
log: GoalLogger;
|
|
26
|
+
/** Override the goal instead of reading ctx.data.goal (mainly for tests). */
|
|
27
|
+
goal?: string | GoalConfig;
|
|
28
|
+
}
|
|
29
|
+
export declare function createGoalStopHook(opts: GoalStopHookOptions): HookHandler;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { normalizeGoal } from "../engine/goal.js";
|
|
2
|
+
const JUDGE_SYSTEM = "你是一个目标完成度裁判。给定一个目标和 agent 最近的输出,判断目标是否已经" +
|
|
3
|
+
"完全达成。只返回一个 JSON 对象,形如 " +
|
|
4
|
+
'{"met": true|false, "gaps": "若未达成,简述还差什么;达成则空串"}。' +
|
|
5
|
+
"不要输出任何额外文字。宁可严格:只有确信目标已完全完成时才返回 met:true。";
|
|
6
|
+
/** Pull the first balanced JSON object out of possibly-prose text. */
|
|
7
|
+
function extractJson(text) {
|
|
8
|
+
const start = text.indexOf("{");
|
|
9
|
+
const end = text.lastIndexOf("}");
|
|
10
|
+
if (start === -1 || end === -1 || end <= start)
|
|
11
|
+
return null;
|
|
12
|
+
const slice = text.slice(start, end + 1);
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(slice);
|
|
15
|
+
if (!parsed || typeof parsed !== "object")
|
|
16
|
+
return null;
|
|
17
|
+
const p = parsed;
|
|
18
|
+
if (typeof p.met !== "boolean")
|
|
19
|
+
return null;
|
|
20
|
+
const gaps = typeof p.gaps === "string" ? p.gaps : "";
|
|
21
|
+
return { met: p.met, gaps };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function createGoalStopHook(opts) {
|
|
28
|
+
const { llm, log } = opts;
|
|
29
|
+
return async (ctx) => {
|
|
30
|
+
// Accept string or GoalConfig from either the override or ctx.data.goal.
|
|
31
|
+
const g = normalizeGoal(opts.goal ?? ctx.data.goal);
|
|
32
|
+
// No goal → not Goal mode → allow stop.
|
|
33
|
+
if (!g)
|
|
34
|
+
return {};
|
|
35
|
+
const goal = g.objective;
|
|
36
|
+
const finalText = typeof ctx.data.finalText === "string" ? ctx.data.finalText : "";
|
|
37
|
+
let verdict = null;
|
|
38
|
+
try {
|
|
39
|
+
const resp = await llm.createMessage({
|
|
40
|
+
systemPrompt: JUDGE_SYSTEM,
|
|
41
|
+
messages: [
|
|
42
|
+
{
|
|
43
|
+
role: "user",
|
|
44
|
+
content: `目标:\n${goal}\n\n` +
|
|
45
|
+
`agent 最近的输出:\n${finalText || "(无文本输出)"}\n\n` +
|
|
46
|
+
"目标完全达成了吗?按要求只返回 JSON。",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
stream: false,
|
|
50
|
+
maxTokens: 400,
|
|
51
|
+
// Auxiliary sub-call — keep it out of the session cost/turn stats.
|
|
52
|
+
recordUsage: false,
|
|
53
|
+
});
|
|
54
|
+
verdict = extractJson(resp.text ?? "");
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
log.warn("goal_stop.judge_failed", {
|
|
58
|
+
cat: "goal",
|
|
59
|
+
error: err.message,
|
|
60
|
+
});
|
|
61
|
+
// P0: do NOT silently allow stop on judge failure — in unattended runs
|
|
62
|
+
// that means the goal silently fails. Nudge to continue instead; the
|
|
63
|
+
// run-scoped budget guardrail (turn-loop) is the real safety backstop
|
|
64
|
+
// that prevents an unsatisfiable goal from looping forever.
|
|
65
|
+
return {
|
|
66
|
+
continueSession: true,
|
|
67
|
+
messages: [
|
|
68
|
+
"继续 —— 目标完成度无法判定,请继续推进直到明确完成(或调用 complete_goal 声明完成)。",
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (!verdict) {
|
|
73
|
+
log.warn("goal_stop.unparseable", { cat: "goal" });
|
|
74
|
+
// P0: same as the throw path — unparseable judge output must not be
|
|
75
|
+
// treated as "done". Continue instead of silently allowing the stop.
|
|
76
|
+
return {
|
|
77
|
+
continueSession: true,
|
|
78
|
+
messages: [
|
|
79
|
+
"继续 —— 目标完成度无法判定,请继续推进直到明确完成(或调用 complete_goal 声明完成)。",
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (verdict.met) {
|
|
84
|
+
log.info("goal_stop.met", { cat: "goal" });
|
|
85
|
+
// Surface the verdict so the loop can emit a goal_progress(met) event.
|
|
86
|
+
return { data: { goalVerdict: { met: true, gaps: "" } } };
|
|
87
|
+
}
|
|
88
|
+
log.info("goal_stop.not_met", { cat: "goal", gaps: verdict.gaps });
|
|
89
|
+
const gaps = verdict.gaps.trim();
|
|
90
|
+
return {
|
|
91
|
+
continueSession: true,
|
|
92
|
+
messages: [
|
|
93
|
+
gaps
|
|
94
|
+
? `继续 —— 目标尚未达成。还差:${gaps}`
|
|
95
|
+
: "继续 —— 目标尚未达成,请接着完成它。",
|
|
96
|
+
],
|
|
97
|
+
// Structured verdict for the UI — the loop emits goal_progress(not_met)
|
|
98
|
+
// with this `gaps` instead of re-running the judge.
|
|
99
|
+
data: { goalVerdict: { met: false, gaps } },
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared output-handling primitives for hook runners.
|
|
3
|
+
*
|
|
4
|
+
* Two runners spawn child processes that produce HookResult JSON:
|
|
5
|
+
* - hooks/shell-runner.ts (user-configured shell hooks from settings)
|
|
6
|
+
* - plugins/pluginCommandHook.ts (hooks declared by installed plugins)
|
|
7
|
+
*
|
|
8
|
+
* Both need the same defences against:
|
|
9
|
+
* 1. Runaway output (a chatty / malicious handler eating engine RAM).
|
|
10
|
+
* 2. Schema drift (unknown decision values, typo'd top-level keys,
|
|
11
|
+
* non-string messages — silent no-ops are worse than rejections).
|
|
12
|
+
*
|
|
13
|
+
* Keeping the byte cap and validator in one module guarantees the two
|
|
14
|
+
* runners can't accidentally diverge on what counts as "valid".
|
|
15
|
+
*/
|
|
16
|
+
import type { HookResult } from "./events.js";
|
|
17
|
+
/**
|
|
18
|
+
* Cap hook output to avoid runaway logging / OOM. 1 MiB is more than enough
|
|
19
|
+
* for any legitimate HookResult (which is typically < 1 KiB), while still
|
|
20
|
+
* absorbing chatty handlers without truncating their decision/messages
|
|
21
|
+
* payload. Past the cap callers kill the child and treat the hook as failed.
|
|
22
|
+
*/
|
|
23
|
+
export declare const MAX_HOOK_OUTPUT_BYTES: number;
|
|
24
|
+
/**
|
|
25
|
+
* Permissive runtime check that a parsed object matches HookResult.
|
|
26
|
+
* Rejects unknown decision values, non-string/array fields, and unknown
|
|
27
|
+
* top-level keys (signalling a typo or hostile/garbage payload). Returns
|
|
28
|
+
* a normalized HookResult on success or null on failure — caller treats
|
|
29
|
+
* null the same as "no result, drop it".
|
|
30
|
+
*
|
|
31
|
+
* Exported so a unit test can pin the contract without spawning a shell.
|
|
32
|
+
*/
|
|
33
|
+
export declare function validateHookResult(parsed: unknown): HookResult | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared output-handling primitives for hook runners.
|
|
3
|
+
*
|
|
4
|
+
* Two runners spawn child processes that produce HookResult JSON:
|
|
5
|
+
* - hooks/shell-runner.ts (user-configured shell hooks from settings)
|
|
6
|
+
* - plugins/pluginCommandHook.ts (hooks declared by installed plugins)
|
|
7
|
+
*
|
|
8
|
+
* Both need the same defences against:
|
|
9
|
+
* 1. Runaway output (a chatty / malicious handler eating engine RAM).
|
|
10
|
+
* 2. Schema drift (unknown decision values, typo'd top-level keys,
|
|
11
|
+
* non-string messages — silent no-ops are worse than rejections).
|
|
12
|
+
*
|
|
13
|
+
* Keeping the byte cap and validator in one module guarantees the two
|
|
14
|
+
* runners can't accidentally diverge on what counts as "valid".
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Cap hook output to avoid runaway logging / OOM. 1 MiB is more than enough
|
|
18
|
+
* for any legitimate HookResult (which is typically < 1 KiB), while still
|
|
19
|
+
* absorbing chatty handlers without truncating their decision/messages
|
|
20
|
+
* payload. Past the cap callers kill the child and treat the hook as failed.
|
|
21
|
+
*/
|
|
22
|
+
export const MAX_HOOK_OUTPUT_BYTES = 1024 * 1024;
|
|
23
|
+
/**
|
|
24
|
+
* Permissive runtime check that a parsed object matches HookResult.
|
|
25
|
+
* Rejects unknown decision values, non-string/array fields, and unknown
|
|
26
|
+
* top-level keys (signalling a typo or hostile/garbage payload). Returns
|
|
27
|
+
* a normalized HookResult on success or null on failure — caller treats
|
|
28
|
+
* null the same as "no result, drop it".
|
|
29
|
+
*
|
|
30
|
+
* Exported so a unit test can pin the contract without spawning a shell.
|
|
31
|
+
*/
|
|
32
|
+
export function validateHookResult(parsed) {
|
|
33
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
34
|
+
return null;
|
|
35
|
+
const p = parsed;
|
|
36
|
+
const known = new Set([
|
|
37
|
+
"stop",
|
|
38
|
+
"continueSession",
|
|
39
|
+
"data",
|
|
40
|
+
"messages",
|
|
41
|
+
"decision",
|
|
42
|
+
"updatedInput",
|
|
43
|
+
"additionalContext",
|
|
44
|
+
"updatedPrompt",
|
|
45
|
+
]);
|
|
46
|
+
for (const k of Object.keys(p)) {
|
|
47
|
+
if (!known.has(k))
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if ("stop" in p && typeof p.stop !== "boolean")
|
|
51
|
+
return null;
|
|
52
|
+
if ("continueSession" in p && typeof p.continueSession !== "boolean")
|
|
53
|
+
return null;
|
|
54
|
+
if ("data" in p) {
|
|
55
|
+
if (!p.data || typeof p.data !== "object" || Array.isArray(p.data))
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if ("messages" in p) {
|
|
59
|
+
if (!Array.isArray(p.messages))
|
|
60
|
+
return null;
|
|
61
|
+
for (const m of p.messages) {
|
|
62
|
+
if (typeof m !== "string")
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if ("decision" in p) {
|
|
67
|
+
if (p.decision !== "allow" && p.decision !== "deny" && p.decision !== "ask")
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if ("updatedInput" in p) {
|
|
71
|
+
if (!p.updatedInput || typeof p.updatedInput !== "object" || Array.isArray(p.updatedInput))
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if ("additionalContext" in p && typeof p.additionalContext !== "string")
|
|
75
|
+
return null;
|
|
76
|
+
if ("updatedPrompt" in p && typeof p.updatedPrompt !== "string")
|
|
77
|
+
return null;
|
|
78
|
+
return p;
|
|
79
|
+
}
|
package/dist/hooks/registry.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ export type HookHandler = (ctx: HookContext) => HookResult | Promise<HookResult>
|
|
|
6
6
|
export declare class HookRegistry {
|
|
7
7
|
private hooks;
|
|
8
8
|
register(eventName: HookEventName, handler: HookHandler, priority?: number, name?: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Remove a previously-registered handler by identity. Used for handlers
|
|
11
|
+
* with a run-scoped lifetime (e.g. the per-run GoalStopHook) so they don't
|
|
12
|
+
* leak across subsequent runs on the same long-lived registry. No-op if
|
|
13
|
+
* the handler isn't registered for that event.
|
|
14
|
+
*/
|
|
15
|
+
unregister(eventName: HookEventName, handler: HookHandler): void;
|
|
9
16
|
emit(eventName: HookEventName, data?: Record<string, unknown>): Promise<HookResult>;
|
|
10
17
|
hasHooks(eventName: HookEventName): boolean;
|
|
11
18
|
clear(eventName?: HookEventName): void;
|