opencode-worktree-guardian 0.1.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/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/codex/.codex-plugin/plugin.json +31 -0
- package/codex/hooks/guardian-hook.ts +265 -0
- package/codex/hooks/hooks.json +30 -0
- package/codex/skills/worktree-guardian/SKILL.md +29 -0
- package/commands/delete-paths.md +12 -0
- package/commands/delete-worktree.md +16 -0
- package/commands/done.md +14 -0
- package/commands/finish-workflow.md +18 -0
- package/commands/finish.md +16 -0
- package/commands/hygiene.md +16 -0
- package/commands/preserve.md +14 -0
- package/commands/recover.md +14 -0
- package/commands/report.md +14 -0
- package/commands/start.md +14 -0
- package/commands/status.md +14 -0
- package/commands/unblock-finish.md +16 -0
- package/docs/adr/0001-guardian-safety-policy.md +192 -0
- package/docs/publishing.md +55 -0
- package/docs/release-checklist.md +84 -0
- package/package.json +72 -0
- package/scripts/readiness.ts +75 -0
- package/scripts/with-safe-node-temp.mjs +78 -0
- package/skills/worktree-guardian/SKILL.md +73 -0
- package/src/config.ts +87 -0
- package/src/delete-paths-apply.ts +77 -0
- package/src/delete-paths-preflight.ts +146 -0
- package/src/delete-paths.ts +1 -0
- package/src/delete-worktree-preflight.ts +25 -0
- package/src/delete-worktree-report.ts +70 -0
- package/src/delete-worktree-targets.ts +152 -0
- package/src/delete-worktree.ts +222 -0
- package/src/delete.ts +1 -0
- package/src/deletion-fingerprint.ts +59 -0
- package/src/done-primary-publish.ts +129 -0
- package/src/done-primary-snapshot.ts +79 -0
- package/src/done-reattach.ts +32 -0
- package/src/done-shared.ts +28 -0
- package/src/done.ts +80 -0
- package/src/filesystem-boundaries.ts +49 -0
- package/src/finish-dirty-files.ts +56 -0
- package/src/finish-report.ts +80 -0
- package/src/finish.ts +212 -0
- package/src/git.ts +288 -0
- package/src/guards/allowlists.ts +83 -0
- package/src/guards/classifier.ts +39 -0
- package/src/guards/destructive-classifier.ts +189 -0
- package/src/guards/git-invocation.ts +145 -0
- package/src/guards/guard-types.ts +36 -0
- package/src/guards/options.ts +15 -0
- package/src/guards/path-policy.ts +31 -0
- package/src/guards/protected-branch-policy.ts +88 -0
- package/src/guards/shell-parser.ts +126 -0
- package/src/guards/shell-prefix.ts +100 -0
- package/src/guards.ts +3 -0
- package/src/hygiene-apply.ts +230 -0
- package/src/hygiene-scan.ts +200 -0
- package/src/hygiene.ts +10 -0
- package/src/index.ts +18 -0
- package/src/lifecycle.ts +31 -0
- package/src/plugin/direct-file-routing.ts +68 -0
- package/src/plugin/event-log.ts +60 -0
- package/src/plugin/guard-context.ts +40 -0
- package/src/plugin/hook-context.ts +35 -0
- package/src/plugin/invisible-policy.ts +28 -0
- package/src/plugin/native-tool.ts +82 -0
- package/src/plugin/plan-token-cache.ts +66 -0
- package/src/plugin/readable-output-cleanup.ts +141 -0
- package/src/plugin/readable-output-status.ts +86 -0
- package/src/plugin/readable-output-values.ts +21 -0
- package/src/plugin/readable-output-workflow.ts +70 -0
- package/src/plugin/readable-output.ts +16 -0
- package/src/plugin/server.ts +257 -0
- package/src/plugin/session-routing.ts +74 -0
- package/src/plugin/slash-commands.ts +20 -0
- package/src/preserve.ts +32 -0
- package/src/recover.ts +195 -0
- package/src/report.ts +168 -0
- package/src/session/context.ts +41 -0
- package/src/session/last-safe-state.ts +27 -0
- package/src/session/worktree-binding.ts +161 -0
- package/src/start.ts +157 -0
- package/src/state.ts +197 -0
- package/src/tool-registry.ts +35 -0
- package/src/tools.ts +8 -0
- package/src/tui.ts +113 -0
- package/src/types.ts +339 -0
- package/src/unblock-finish.ts +298 -0
- package/src/workflow-candidates.ts +111 -0
- package/src/workflow.ts +84 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { loadConfig } from "../config.ts";
|
|
2
|
+
import { getCurrentBranch, listWorktrees } from "../git.ts";
|
|
3
|
+
import { getGuardianPaths, readState } from "../state.ts";
|
|
4
|
+
import type { GuardianConfig, WorktreeEntry } from "../types.ts";
|
|
5
|
+
import { pathExists } from "./session-routing.ts";
|
|
6
|
+
|
|
7
|
+
export async function collectRecordedBranches(repoRoot: string, config: GuardianConfig) {
|
|
8
|
+
const state = await readState(await getGuardianPaths(repoRoot), { repoRoot, config });
|
|
9
|
+
const sessions = Object.values(state.sessions ?? {});
|
|
10
|
+
return [...new Set(sessions.map((session) => session.branch).filter((branch): branch is string => typeof branch === "string" && branch.length > 0))];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function collectProtectedBranchWorktrees(repoRoot: string, config: GuardianConfig) {
|
|
14
|
+
const protectedBranches = Array.isArray(config.protectedBranches) ? config.protectedBranches : [];
|
|
15
|
+
return (await listWorktrees(repoRoot))
|
|
16
|
+
.filter((entry: WorktreeEntry) => typeof entry.branch === "string" && protectedBranches.includes(entry.branch))
|
|
17
|
+
.map((entry: WorktreeEntry) => entry.path)
|
|
18
|
+
.filter((entry: unknown): entry is string => typeof entry === "string");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function collectGuardContext(input: {
|
|
22
|
+
readonly pluginDirectory: string | undefined;
|
|
23
|
+
readonly effectiveCwd: string;
|
|
24
|
+
}) {
|
|
25
|
+
let guardConfig: GuardianConfig | null = null;
|
|
26
|
+
let guardianBranches: string[] = [];
|
|
27
|
+
let protectedBranchWorktreePaths: string[] = [];
|
|
28
|
+
try {
|
|
29
|
+
if (input.pluginDirectory !== undefined && await pathExists(input.pluginDirectory)) {
|
|
30
|
+
guardConfig = (await loadConfig(input.pluginDirectory)).config;
|
|
31
|
+
guardianBranches = await collectRecordedBranches(input.pluginDirectory, guardConfig);
|
|
32
|
+
protectedBranchWorktreePaths = await collectProtectedBranchWorktrees(input.pluginDirectory, guardConfig);
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
let currentBranch: string | null = null;
|
|
36
|
+
try {
|
|
37
|
+
currentBranch = await getCurrentBranch(input.effectiveCwd);
|
|
38
|
+
} catch {}
|
|
39
|
+
return { guardConfig, guardianBranches, protectedBranchWorktreePaths, currentBranch };
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { GuardCommandPayload, HookContext, RecordLike } from "../types.ts";
|
|
2
|
+
import { isRecordLike } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
export function getSessionId(input: GuardCommandPayload = {}): unknown {
|
|
5
|
+
return input.sessionID ?? input.sessionId;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getStringSessionId(input: GuardCommandPayload = {}): string | null {
|
|
9
|
+
const sessionId = getSessionId(input);
|
|
10
|
+
return typeof sessionId === "string" ? sessionId : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getIdleEventSessionId(input: RecordLike): string | null {
|
|
14
|
+
const event = isRecordLike(input.event) ? input.event : null;
|
|
15
|
+
const properties = isRecordLike(event?.properties) ? event.properties : {};
|
|
16
|
+
const sessionId = properties.sessionID ?? properties.sessionId ?? event?.sessionID ?? event?.sessionId;
|
|
17
|
+
return typeof sessionId === "string" ? sessionId : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function firstString(...values: readonly unknown[]): string | undefined {
|
|
21
|
+
return values.find((value): value is string => typeof value === "string");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getExecutionCwd(input: GuardCommandPayload = {}, output: GuardCommandPayload = {}, context: HookContext = {}) {
|
|
25
|
+
return firstString(
|
|
26
|
+
output.args?.workdir,
|
|
27
|
+
output.args?.cwd,
|
|
28
|
+
input.args?.workdir,
|
|
29
|
+
input.args?.cwd,
|
|
30
|
+
input.workdir,
|
|
31
|
+
input.cwd,
|
|
32
|
+
context.worktree,
|
|
33
|
+
context.directory,
|
|
34
|
+
) ?? process.cwd();
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GuardianConfig, GuardianToolInput } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export function buildInvisiblePolicy(config: GuardianConfig) {
|
|
4
|
+
return [
|
|
5
|
+
"Worktree Guardian policy:",
|
|
6
|
+
"- Guardian auto-starts session worktree ownership by default; repo config autoStart=false disables automatic ownership.",
|
|
7
|
+
"- Do not run raw destructive git cleanup, reset, stash mutation, force-push, protected branches mutation, worktree removal, or rm -rf against worktrees.",
|
|
8
|
+
"- Finish normal Guardian work through guardian_done so Guardian can choose the safe lane; use guardian_finish only for explicit low-level session finishing.",
|
|
9
|
+
"- Use guardian_status for read-only inspection and guardian_done for normal gated completion.",
|
|
10
|
+
"- Safe mutating shell/git tool calls for a recorded Guardian session are routed into that recorded worktree automatically.",
|
|
11
|
+
`- Default finish mode is ${config.finishMode}; auto-finish is ${config.autoFinish ? "enabled by repo config" : "disabled"} unless repo config opts in.`,
|
|
12
|
+
].join("\n");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function injectInvisiblePolicy(output: GuardianToolInput | null | undefined, config: GuardianConfig) {
|
|
16
|
+
if (!output || typeof output !== "object") return false;
|
|
17
|
+
const policy = buildInvisiblePolicy(config);
|
|
18
|
+
if (Array.isArray(output.system)) {
|
|
19
|
+
output.system.push(policy);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (typeof output.system === "string") {
|
|
23
|
+
output.system = `${output.system}\n\n${policy}`;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
output.system = [policy];
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { resolveSessionWorktree } from "../session/worktree-binding.ts";
|
|
3
|
+
import { runGuardianTool } from "../tool-registry.ts";
|
|
4
|
+
import type { GuardianNativeToolReturn, GuardianToolInput, GuardianToolName, HookContext, PlanCacheToolArgs, PlanTokenCache } from "../types.ts";
|
|
5
|
+
import { normalizeOptionalToolStrings, maybeInjectPlanConfirmToken, rememberPlanConfirmToken } from "./plan-token-cache.ts";
|
|
6
|
+
import { formatGuardianOutput, READABLE_GUARDIAN_TOOLS } from "./readable-output.ts";
|
|
7
|
+
import { resolveActualWorktreeOrPath } from "./session-routing.ts";
|
|
8
|
+
|
|
9
|
+
const z = tool.schema;
|
|
10
|
+
const SESSION_WORKTREE_DEFAULT_TOOLS = new Set(["guardian_finish", "guardian_preserve"]);
|
|
11
|
+
|
|
12
|
+
async function getRecordedToolWorktree(name: GuardianToolName, toolArgs: PlanCacheToolArgs, context: HookContext) {
|
|
13
|
+
if (!SESSION_WORKTREE_DEFAULT_TOOLS.has(name)) return null;
|
|
14
|
+
if (typeof toolArgs.repoRoot !== "string" || typeof toolArgs.sessionId !== "string") return null;
|
|
15
|
+
const contextCwd = typeof context?.worktree === "string" ? context.worktree : typeof context?.directory === "string" ? context.directory : toolArgs.repoRoot;
|
|
16
|
+
const actualWorktree = await resolveActualWorktreeOrPath(contextCwd);
|
|
17
|
+
const sessionWorktree = await resolveSessionWorktree({
|
|
18
|
+
repoRoot: toolArgs.repoRoot,
|
|
19
|
+
cwd: contextCwd,
|
|
20
|
+
actualWorktree,
|
|
21
|
+
sessionId: toolArgs.sessionId,
|
|
22
|
+
});
|
|
23
|
+
return typeof sessionWorktree?.expectedWorktree === "string" ? sessionWorktree.expectedWorktree : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function guardianTool(name: GuardianToolName, description: string, planCache?: PlanTokenCache) {
|
|
27
|
+
return tool({
|
|
28
|
+
description,
|
|
29
|
+
args: {
|
|
30
|
+
repoRoot: z.string().optional(),
|
|
31
|
+
cwd: z.string().optional(),
|
|
32
|
+
sessionId: z.string().optional(),
|
|
33
|
+
taskName: z.string().optional(),
|
|
34
|
+
branch: z.string().optional(),
|
|
35
|
+
targetPath: z.string().optional(),
|
|
36
|
+
worktreePath: z.string().optional(),
|
|
37
|
+
createWorktree: z.boolean().optional(),
|
|
38
|
+
mode: z.enum(["plan", "apply"]).optional(),
|
|
39
|
+
confirmToken: z.string().optional(),
|
|
40
|
+
confirmDelete: z.boolean().optional(),
|
|
41
|
+
confirm: z.boolean().optional(),
|
|
42
|
+
action: z.enum(["commit-review-artifacts"]).optional(),
|
|
43
|
+
commitMessage: z.string().optional(),
|
|
44
|
+
deleteBranch: z.boolean().optional(),
|
|
45
|
+
abandonUnmerged: z.boolean().optional(),
|
|
46
|
+
allowIgnoredFiles: z.boolean().optional(),
|
|
47
|
+
paths: z.array(z.string()).optional(),
|
|
48
|
+
allowTracked: z.boolean().optional(),
|
|
49
|
+
allowRecursive: z.boolean().optional(),
|
|
50
|
+
cleanupPaths: z.array(z.string()).optional(),
|
|
51
|
+
allowCategories: z.array(z.enum(["known-cleanable", "nested-git", "suspicious"])).optional(),
|
|
52
|
+
allowDirtyNestedGit: z.boolean().optional(),
|
|
53
|
+
timestamp: z.string().optional(),
|
|
54
|
+
finishMode: z.enum(["preserve-only", "push-branch", "create-pr", "merge-to-base"]).optional(),
|
|
55
|
+
allowMergeToBase: z.boolean().optional(),
|
|
56
|
+
},
|
|
57
|
+
async execute(args: GuardianToolInput, context: HookContext): Promise<GuardianNativeToolReturn> {
|
|
58
|
+
context.metadata?.({ title: name });
|
|
59
|
+
const toolArgs = { ...args };
|
|
60
|
+
normalizeOptionalToolStrings(toolArgs);
|
|
61
|
+
if (toolArgs.repoRoot == null && typeof context?.directory === "string") toolArgs.repoRoot = context.directory;
|
|
62
|
+
if (toolArgs.sessionId == null && (name === "guardian_unblock_finish" || toolArgs.targetPath == null && toolArgs.branch == null)) {
|
|
63
|
+
if (typeof context?.sessionID === "string") toolArgs.sessionId = context.sessionID;
|
|
64
|
+
else if (typeof context?.sessionId === "string") toolArgs.sessionId = context.sessionId;
|
|
65
|
+
}
|
|
66
|
+
if (toolArgs.cwd == null) {
|
|
67
|
+
const recordedWorktree = await getRecordedToolWorktree(name, toolArgs, context);
|
|
68
|
+
if (recordedWorktree) toolArgs.cwd = recordedWorktree;
|
|
69
|
+
else if (typeof context?.worktree === "string") toolArgs.cwd = context.worktree;
|
|
70
|
+
else if (typeof context?.directory === "string") toolArgs.cwd = context.directory;
|
|
71
|
+
}
|
|
72
|
+
maybeInjectPlanConfirmToken(name, toolArgs, planCache);
|
|
73
|
+
const result = await runGuardianTool(name, toolArgs);
|
|
74
|
+
rememberPlanConfirmToken(name, toolArgs, result, planCache);
|
|
75
|
+
return {
|
|
76
|
+
title: name,
|
|
77
|
+
metadata: result,
|
|
78
|
+
output: READABLE_GUARDIAN_TOOLS.has(name) ? formatGuardianOutput(name, result) : JSON.stringify(result, null, 2),
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { GuardCommandPayload, GuardianToolName, PlanCacheToolArgs, PlanTokenCache } from "../types.ts";
|
|
2
|
+
import { isMutableRecord } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
export function ensureToolArgs(output: GuardCommandPayload = {}) {
|
|
5
|
+
if (!isMutableRecord(output.args)) output.args = {};
|
|
6
|
+
return output.args;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function sortedStringArgs(value: unknown) {
|
|
10
|
+
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string").sort((left, right) => left.localeCompare(right)) : [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeOptionalToolStrings(toolArgs: PlanCacheToolArgs) {
|
|
14
|
+
for (const key of ["repoRoot", "cwd", "sessionId", "branch", "targetPath", "worktreePath", "confirmToken"]) {
|
|
15
|
+
if (typeof toolArgs[key] === "string" && toolArgs[key].trim() === "") delete toolArgs[key];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function planCacheKey(name: GuardianToolName, toolArgs: PlanCacheToolArgs) {
|
|
20
|
+
return JSON.stringify({
|
|
21
|
+
name,
|
|
22
|
+
sessionId: typeof toolArgs.sessionId === "string" ? toolArgs.sessionId : "",
|
|
23
|
+
repoRoot: typeof toolArgs.repoRoot === "string" ? toolArgs.repoRoot : "",
|
|
24
|
+
cwd: typeof toolArgs.cwd === "string" ? toolArgs.cwd : "",
|
|
25
|
+
paths: sortedStringArgs(toolArgs.paths),
|
|
26
|
+
cleanupPaths: sortedStringArgs(toolArgs.cleanupPaths),
|
|
27
|
+
allowCategories: sortedStringArgs(toolArgs.allowCategories),
|
|
28
|
+
allowTracked: toolArgs.allowTracked === true,
|
|
29
|
+
allowRecursive: toolArgs.allowRecursive === true,
|
|
30
|
+
allowDirtyNestedGit: toolArgs.allowDirtyNestedGit === true,
|
|
31
|
+
commitMessage: typeof toolArgs.commitMessage === "string" ? toolArgs.commitMessage : "",
|
|
32
|
+
finishMode: typeof toolArgs.finishMode === "string" ? toolArgs.finishMode : "",
|
|
33
|
+
deleteBranch: toolArgs.deleteBranch === true,
|
|
34
|
+
abandonUnmerged: toolArgs.abandonUnmerged === true,
|
|
35
|
+
allowIgnoredFiles: toolArgs.allowIgnoredFiles === true,
|
|
36
|
+
action: typeof toolArgs.action === "string" ? toolArgs.action : "",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isPlaceholderConfirmToken(value: unknown) {
|
|
41
|
+
if (typeof value !== "string") return false;
|
|
42
|
+
const normalized = value.trim();
|
|
43
|
+
return normalized === "" || normalized === "CONFIRM_DELETE";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function shouldUseCachedPlanToken(name: GuardianToolName, toolArgs: PlanCacheToolArgs) {
|
|
47
|
+
if (toolArgs.mode !== "apply") return false;
|
|
48
|
+
if (name === "guardian_delete_paths") return toolArgs.confirmDelete === true;
|
|
49
|
+
if (name === "guardian_hygiene") return toolArgs.confirmDelete === true;
|
|
50
|
+
if (name === "guardian_done" || name === "guardian_finish_workflow") return toolArgs.confirm === true || toolArgs.confirmDelete === true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function maybeInjectPlanConfirmToken(name: GuardianToolName, toolArgs: PlanCacheToolArgs, planCache?: PlanTokenCache) {
|
|
55
|
+
if (!planCache || !shouldUseCachedPlanToken(name, toolArgs)) return;
|
|
56
|
+
if (typeof toolArgs.confirmToken === "string" && !isPlaceholderConfirmToken(toolArgs.confirmToken)) return;
|
|
57
|
+
const cachedToken = planCache.get(planCacheKey(name, toolArgs));
|
|
58
|
+
if (cachedToken) toolArgs.confirmToken = cachedToken;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function rememberPlanConfirmToken(name: GuardianToolName, toolArgs: PlanCacheToolArgs, result: { readonly ok?: unknown; readonly status?: unknown; readonly confirmToken?: unknown }, planCache?: PlanTokenCache) {
|
|
62
|
+
if (!planCache) return;
|
|
63
|
+
if (toolArgs.mode !== "plan" || result.ok !== true || result.status !== "planned" || typeof result.confirmToken !== "string") return;
|
|
64
|
+
if (!["guardian_delete_paths", "guardian_hygiene", "guardian_done", "guardian_finish_workflow"].includes(name)) return;
|
|
65
|
+
planCache.set(planCacheKey(name, toolArgs), result.confirmToken);
|
|
66
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { arrayValue, recordValue, shortCommit, textValue } from "./readable-output-values.ts";
|
|
2
|
+
|
|
3
|
+
export function formatGuardianHygieneOutput(rawResult: unknown) {
|
|
4
|
+
const result = recordValue(rawResult);
|
|
5
|
+
if (["planned", "cleaned", "blocked"].includes(textValue(result.status, ""))) return formatGuardianHygienePlanOutput(rawResult);
|
|
6
|
+
const summary = recordValue(result.summary);
|
|
7
|
+
const findings = arrayValue(result.findings);
|
|
8
|
+
const exclusions = arrayValue(result.exclusions);
|
|
9
|
+
const failCount = Number(recordValue(summary.bySeverity).fail ?? 0);
|
|
10
|
+
const warnCount = Number(recordValue(summary.bySeverity).warn ?? 0);
|
|
11
|
+
const scanFailed = result.ok === false || summary.scanFailed === true;
|
|
12
|
+
const lines = [`${scanFailed ? "[FAIL]" : findings.length > 0 ? "[WARN]" : "[GOOD]"} guardian_hygiene scan`, `[INFO] repoRoot: ${textValue(result.repoRoot)}`];
|
|
13
|
+
if (scanFailed) lines.push("[WARN] scan incomplete: findings and candidate counts are not trustworthy");
|
|
14
|
+
else lines.push(`[INFO] findings: ${Number(summary.findingCount ?? findings.length)} | warn: ${warnCount} | fail: ${failCount} | exclusions: ${Number(summary.exclusionCount ?? exclusions.length)} | candidates: ${Number(summary.candidateCount ?? 0)}`);
|
|
15
|
+
const reason = textValue(result.reason, "");
|
|
16
|
+
if (result.ok === false || reason) lines.push(`[FAIL] ${reason || "guardian_hygiene scan failed"}`);
|
|
17
|
+
if (findings.length > 0) {
|
|
18
|
+
lines.push("[WARN] top findings:");
|
|
19
|
+
for (const entry of findings.slice(0, 8)) {
|
|
20
|
+
const finding = recordValue(entry);
|
|
21
|
+
lines.push(` - ${textValue(finding.severity)} ${textValue(finding.category)} ${textValue(finding.path)}: ${textValue(finding.reason)}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const suggestions = arrayValue(result.suggestedCommands);
|
|
25
|
+
if (suggestions.length > 0) {
|
|
26
|
+
lines.push("[INFO] suggested commands:");
|
|
27
|
+
for (const command of suggestions.slice(0, 8)) lines.push(` - ${textValue(command, String(command))}`);
|
|
28
|
+
}
|
|
29
|
+
return lines.join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatGuardianHygienePlanOutput(rawResult: unknown) {
|
|
33
|
+
const result = recordValue(rawResult);
|
|
34
|
+
const summary = recordValue(result.summary);
|
|
35
|
+
const targets = arrayValue(result.targets);
|
|
36
|
+
const removedTargets = arrayValue(result.removedTargets);
|
|
37
|
+
const blockers = arrayValue(result.blockers);
|
|
38
|
+
const lines = [
|
|
39
|
+
`${result.ok === false ? "[FAIL]" : result.status === "planned" ? "[WARN]" : "[GOOD]"} guardian_hygiene ${textValue(result.status)}`,
|
|
40
|
+
`[INFO] approvedTargets: ${Number(summary.approvedTargetCount ?? targets.length)} | removedTargets: ${Number(summary.removedTargetCount ?? removedTargets.length)} | blockers: ${Number(summary.blockedTargetCount ?? blockers.length)} | fatal: ${Number(summary.fatalBlockerCount ?? 0)}`,
|
|
41
|
+
];
|
|
42
|
+
const reason = textValue(result.reason, "");
|
|
43
|
+
if (result.ok === false || reason) lines.push(`[FAIL] ${reason || "guardian_hygiene blocked"}`);
|
|
44
|
+
if (targets.length > 0) {
|
|
45
|
+
lines.push("[INFO] approved targets:");
|
|
46
|
+
for (const entry of targets.slice(0, 8)) {
|
|
47
|
+
const target = recordValue(entry);
|
|
48
|
+
lines.push(` - ${textValue(target.category)} ${textValue(target.path)}: ${textValue(target.reason)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (blockers.length > 0) {
|
|
52
|
+
lines.push("[WARN] blockers:");
|
|
53
|
+
for (const entry of blockers.slice(0, 8)) {
|
|
54
|
+
const blocker = recordValue(entry);
|
|
55
|
+
lines.push(` - ${blocker.fatal === true ? "fatal" : "blocked"} ${textValue(blocker.path)}: ${textValue(blocker.reason)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatGuardianDeleteOutput(rawResult: unknown) {
|
|
62
|
+
const result = recordValue(rawResult);
|
|
63
|
+
const preflight = recordValue(result.preflight);
|
|
64
|
+
const lines = [
|
|
65
|
+
`${result.ok === false ? "[FAIL]" : result.status === "planned" ? "[WARN]" : "[GOOD]"} guardian_delete_worktree ${textValue(result.status)}`,
|
|
66
|
+
`[INFO] mode: ${textValue(preflight.mode)} | targetKind: ${textValue(preflight.targetKind, "worktree")} | deleteBranch: ${String(preflight.deleteBranch === true)} | abandonUnmerged: ${String(preflight.abandonUnmerged === true)} | branchDeleted: ${String(result.branchDeleted === true)} | worktreeRemoved: ${String(result.worktreeRemoved === true)}`,
|
|
67
|
+
`[INFO] targetPath: ${textValue(preflight.targetPath ?? result.targetPath)}`,
|
|
68
|
+
`[INFO] branch: ${textValue(preflight.branch ?? result.branch)} | head: ${shortCommit(preflight.head ?? result.head)}`,
|
|
69
|
+
];
|
|
70
|
+
if (preflight.ancestryProven === false || Number(preflight.unmergedCommitCount ?? 0) > 0) {
|
|
71
|
+
lines.push(`[WARN] ancestryProven: ${String(preflight.ancestryProven === true)} | ancestryRef: ${textValue(preflight.ancestryRef)} | unmergedCommitCount: ${Number(preflight.unmergedCommitCount ?? 0)}`);
|
|
72
|
+
}
|
|
73
|
+
const reason = textValue(result.reason, "");
|
|
74
|
+
if (result.ok === false || reason) lines.push(`[FAIL] ${reason || "guardian_delete_worktree blocked"}`);
|
|
75
|
+
if (typeof result.confirmToken === "string") lines.push(`[WARN] confirmToken: ${result.confirmToken}`);
|
|
76
|
+
if (typeof result.safetyRef === "string") lines.push(`[INFO] safetyRef: ${result.safetyRef}`);
|
|
77
|
+
const blockers = arrayValue(preflight.blockers);
|
|
78
|
+
if (blockers.length > 0) {
|
|
79
|
+
lines.push("[WARN] blockers:");
|
|
80
|
+
for (const blocker of blockers.slice(0, 8)) lines.push(` - ${textValue(blocker, String(blocker))}`);
|
|
81
|
+
}
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatGuardianDeletePathsOutput(rawResult: unknown) {
|
|
86
|
+
const result = recordValue(rawResult);
|
|
87
|
+
const summary = recordValue(result.summary);
|
|
88
|
+
const targets = arrayValue(result.targets);
|
|
89
|
+
const removedTargets = arrayValue(result.removedTargets);
|
|
90
|
+
const blockers = arrayValue(result.blockers);
|
|
91
|
+
const preflight = recordValue(result.preflight);
|
|
92
|
+
const lines = [
|
|
93
|
+
`${result.ok === false ? "[FAIL]" : result.status === "planned" ? "[WARN]" : "[GOOD]"} guardian_delete_paths ${textValue(result.status)}`,
|
|
94
|
+
`[INFO] paths: ${arrayValue(preflight.paths).length} | approvedTargets: ${Number(summary.approvedTargetCount ?? targets.length)} | removedTargets: ${Number(summary.removedTargetCount ?? removedTargets.length)} | blockers: ${Number(summary.blockedTargetCount ?? blockers.length)} | fatal: ${Number(summary.fatalBlockerCount ?? 0)}`,
|
|
95
|
+
`[INFO] allowTracked: ${String(preflight.allowTracked === true)} | allowRecursive: ${String(preflight.allowRecursive === true)}`,
|
|
96
|
+
];
|
|
97
|
+
const reason = textValue(result.reason, "");
|
|
98
|
+
if (result.ok === false || reason) lines.push(`[FAIL] ${reason || "guardian_delete_paths blocked"}`);
|
|
99
|
+
if (targets.length > 0) {
|
|
100
|
+
lines.push("[INFO] approved targets:");
|
|
101
|
+
for (const entry of targets.slice(0, 8)) {
|
|
102
|
+
const target = recordValue(entry);
|
|
103
|
+
lines.push(` - ${textValue(target.status)} ${textValue(target.kind)} ${textValue(target.path)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (blockers.length > 0) {
|
|
107
|
+
lines.push("[WARN] blockers:");
|
|
108
|
+
for (const entry of blockers.slice(0, 8)) {
|
|
109
|
+
const blocker = recordValue(entry);
|
|
110
|
+
lines.push(` - ${blocker.fatal === true ? "fatal" : "blocked"} ${textValue(blocker.path)}: ${textValue(blocker.reason)}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function formatGuardianUnblockFinishOutput(rawResult: unknown) {
|
|
117
|
+
const result = recordValue(rawResult);
|
|
118
|
+
const preflight = recordValue(result.preflight);
|
|
119
|
+
const lines = [
|
|
120
|
+
`${result.ok === false ? "[FAIL]" : result.status === "planned" ? "[WARN]" : "[GOOD]"} guardian_unblock_finish ${textValue(result.status)}`,
|
|
121
|
+
`[INFO] action: ${textValue(result.action ?? preflight.action)} | sessionId: ${textValue(preflight.sessionId)} | branch: ${textValue(preflight.branch)}`,
|
|
122
|
+
`[INFO] worktreePath: ${textValue(preflight.worktreePath)}`,
|
|
123
|
+
];
|
|
124
|
+
const reviewArtifactPaths = arrayValue(preflight.reviewArtifactPaths);
|
|
125
|
+
if (reviewArtifactPaths.length > 0) {
|
|
126
|
+
lines.push(`[INFO] review artifacts: ${reviewArtifactPaths.length}`);
|
|
127
|
+
for (const entry of reviewArtifactPaths.slice(0, 8)) lines.push(` - ${textValue(entry, String(entry))}`);
|
|
128
|
+
}
|
|
129
|
+
const otherDirtyPaths = arrayValue(preflight.otherDirtyPaths);
|
|
130
|
+
if (otherDirtyPaths.length > 0) {
|
|
131
|
+
lines.push(`[WARN] other dirty paths: ${otherDirtyPaths.length}`);
|
|
132
|
+
for (const entry of otherDirtyPaths.slice(0, 8)) lines.push(` - ${textValue(entry, String(entry))}`);
|
|
133
|
+
}
|
|
134
|
+
const reason = textValue(result.reason, "");
|
|
135
|
+
if (result.ok === false || reason) lines.push(`[FAIL] ${reason || "guardian_unblock_finish blocked"}`);
|
|
136
|
+
if (typeof result.nextAction === "string") lines.push(`[INFO] nextAction: ${result.nextAction}`);
|
|
137
|
+
if (typeof result.commitMessage === "string") lines.push(`[INFO] commitMessage: ${result.commitMessage}`);
|
|
138
|
+
if (typeof result.commit === "string") lines.push(`[INFO] commit: ${shortCommit(result.commit)}`);
|
|
139
|
+
if (typeof result.safetyRef === "string") lines.push(`[INFO] safetyRef: ${result.safetyRef}`);
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { arrayValue, describeEntry, recordValue, shortCommit, textValue } from "./readable-output-values.ts";
|
|
2
|
+
|
|
3
|
+
function countLine(result: Record<string, unknown>) {
|
|
4
|
+
const counts = [
|
|
5
|
+
["sessions", arrayValue(result.sessions).length],
|
|
6
|
+
["worktrees", arrayValue(result.worktrees).length],
|
|
7
|
+
["orphaned", arrayValue(result.orphanedSessions).length],
|
|
8
|
+
["poisoned", arrayValue(result.poisonedSessions).length],
|
|
9
|
+
["dirty", arrayValue(result.dirtyFiles).length],
|
|
10
|
+
["stashes", arrayValue(result.stashes).length],
|
|
11
|
+
["safetyRefs", arrayValue(result.safetyRefs).length],
|
|
12
|
+
["preservedRefs", arrayValue(result.preservedRefs).length],
|
|
13
|
+
["recoveryCandidates", arrayValue(result.recoveryCandidates).length],
|
|
14
|
+
];
|
|
15
|
+
return counts.map(([label, count]) => `${label}: ${count}`).join(" | ");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatGuardianStatusOutput(name: string, rawResult: unknown) {
|
|
19
|
+
const result = recordValue(rawResult);
|
|
20
|
+
const lines = [
|
|
21
|
+
`${result.ok === false ? "[FAIL]" : "[GOOD]"} ${name} snapshot`,
|
|
22
|
+
`[INFO] repoRoot: ${textValue(result.repoRoot)}`,
|
|
23
|
+
`[INFO] ${countLine(result)}`,
|
|
24
|
+
];
|
|
25
|
+
const reason = textValue(result.reason, "");
|
|
26
|
+
if (result.ok === false || reason) lines.push(`[FAIL] ${reason || "guardian tool reported failure"}`);
|
|
27
|
+
const warningSections = [
|
|
28
|
+
["orphaned sessions", result.orphanedSessions],
|
|
29
|
+
["poisoned sessions", result.poisonedSessions],
|
|
30
|
+
["worktrees without state", result.worktreesWithoutState],
|
|
31
|
+
["state branches without worktrees", result.stateBranchesWithoutWorktrees],
|
|
32
|
+
["dirty files", result.dirtyFiles],
|
|
33
|
+
["stashes", result.stashes],
|
|
34
|
+
];
|
|
35
|
+
for (const [label, value] of warningSections) {
|
|
36
|
+
const entries = arrayValue(value);
|
|
37
|
+
if (entries.length > 0) {
|
|
38
|
+
lines.push(`[WARN] ${label}: ${entries.length}`);
|
|
39
|
+
for (const entry of entries.slice(0, 8)) lines.push(` - ${describeEntry(entry)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const activeSessions = arrayValue(result.activeSessions);
|
|
43
|
+
const terminalSessions = arrayValue(result.terminalSessions);
|
|
44
|
+
const sessions = arrayValue(result.sessions);
|
|
45
|
+
const visibleActiveSessions = activeSessions.length > 0 ? activeSessions : sessions.filter((entry) => recordValue(entry).status === "active");
|
|
46
|
+
lines.push(`[INFO] active sessions: ${visibleActiveSessions.length}`);
|
|
47
|
+
for (const entry of visibleActiveSessions.slice(0, 12)) {
|
|
48
|
+
const session = recordValue(entry);
|
|
49
|
+
lines.push(` - session_id=${textValue(session.session_id ?? session.sessionId)} status=${textValue(session.status)} branch=${textValue(session.branch)} worktree_path=${textValue(session.worktree_path ?? session.worktreePath)} head=${shortCommit(session.head_commit ?? session.headCommit)}`);
|
|
50
|
+
}
|
|
51
|
+
lines.push(`[INFO] terminal sessions: ${terminalSessions.length}`);
|
|
52
|
+
for (const entry of terminalSessions.slice(0, 12)) {
|
|
53
|
+
const session = recordValue(entry);
|
|
54
|
+
lines.push(` - session_id=${textValue(session.session_id ?? session.sessionId)} status=${textValue(session.status)} branch=${textValue(session.branch)} worktree_path=${textValue(session.worktree_path ?? session.worktreePath)} head=${shortCommit(session.head_commit ?? session.headCommit)}`);
|
|
55
|
+
}
|
|
56
|
+
const worktrees = arrayValue(result.worktrees);
|
|
57
|
+
lines.push(`[INFO] worktrees: ${worktrees.length}`);
|
|
58
|
+
for (const entry of worktrees.slice(0, 12)) {
|
|
59
|
+
const worktree = recordValue(entry);
|
|
60
|
+
const markers = [worktree.detached === true ? "detached" : "", worktree.bare === true ? "bare" : ""].filter(Boolean).join(",");
|
|
61
|
+
lines.push(` - branch=${textValue(worktree.branch)} head=${shortCommit(worktree.head ?? worktree.head_commit ?? worktree.headCommit)} path=${textValue(worktree.path ?? worktree.worktree_path ?? worktree.worktreePath)}${markers ? ` markers=${markers}` : ""}`);
|
|
62
|
+
}
|
|
63
|
+
const recoveryCandidates = arrayValue(result.recoveryCandidates);
|
|
64
|
+
if (recoveryCandidates.length > 0) {
|
|
65
|
+
lines.push(`[INFO] recovery candidates: ${recoveryCandidates.length}`);
|
|
66
|
+
for (const entry of recoveryCandidates.slice(0, 12)) lines.push(` - ${describeEntry(entry)}`);
|
|
67
|
+
}
|
|
68
|
+
const suggestions = arrayValue(result.suggestedCommands);
|
|
69
|
+
if (suggestions.length > 0) {
|
|
70
|
+
lines.push("[INFO] suggested commands:");
|
|
71
|
+
for (const command of suggestions) lines.push(` - ${textValue(command, String(command))}`);
|
|
72
|
+
}
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function formatGuardianReportOutput(rawResult: unknown) {
|
|
77
|
+
const result = recordValue(rawResult);
|
|
78
|
+
const status = recordValue(result.status);
|
|
79
|
+
const recover = recordValue(result.recover);
|
|
80
|
+
return [
|
|
81
|
+
`${result.ok === false ? "[FAIL]" : "[GOOD]"} guardian_report_html wrote offline report`,
|
|
82
|
+
`[INFO] reportPath: ${textValue(result.reportPath)}`,
|
|
83
|
+
`[INFO] repoRoot: ${textValue(status.repoRoot)}`,
|
|
84
|
+
`[INFO] sessions: ${arrayValue(status.sessions).length} | worktrees: ${arrayValue(status.worktrees).length} | risks: ${arrayValue(status.orphanedSessions).length + arrayValue(status.worktreesWithoutState).length + arrayValue(status.dirtyFiles).length + arrayValue(status.stashes).length} | recoveryCandidates: ${arrayValue(recover.recoveryCandidates).length}`,
|
|
85
|
+
].join("\n");
|
|
86
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function recordValue(value: unknown): Record<string, unknown> {
|
|
2
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function arrayValue(value: unknown): unknown[] {
|
|
6
|
+
return Array.isArray(value) ? value : [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function textValue(value: unknown, fallback = "-") {
|
|
10
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function shortCommit(value: unknown) {
|
|
14
|
+
const text = textValue(value);
|
|
15
|
+
return text === "-" ? text : text.slice(0, 12);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function describeEntry(entry: unknown) {
|
|
19
|
+
const item = recordValue(entry);
|
|
20
|
+
return textValue(item.session_id ?? item.sessionId ?? item.branch ?? item.path ?? item.worktree_path ?? item.name ?? item.ref ?? item.command ?? entry, JSON.stringify(entry));
|
|
21
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { arrayValue, recordValue, shortCommit, textValue } from "./readable-output-values.ts";
|
|
2
|
+
|
|
3
|
+
export function formatGuardianFinishWorkflowOutput(rawResult: unknown) {
|
|
4
|
+
const result = recordValue(rawResult);
|
|
5
|
+
const preflight = recordValue(result.preflight);
|
|
6
|
+
const candidates = arrayValue(result.candidates);
|
|
7
|
+
const blockers = arrayValue(result.blockers);
|
|
8
|
+
const results = arrayValue(result.results);
|
|
9
|
+
const lines = [
|
|
10
|
+
`${result.ok === false ? "[FAIL]" : result.status === "planned" ? "[WARN]" : "[GOOD]"} guardian_finish_workflow ${textValue(result.status)}`,
|
|
11
|
+
`[INFO] mode: ${textValue(preflight.mode)} | branch: ${textValue(preflight.currentBranch)} | baseRef: ${textValue(preflight.baseRef)} | baseRefOid: ${shortCommit(preflight.baseRefOid)}`,
|
|
12
|
+
`[INFO] candidates: ${candidates.length} | blockers: ${blockers.length} | maxCandidates: ${Number(preflight.maxCandidateCount ?? 0)} | dirty: ${Number(preflight.dirtyFileCount ?? 0)} | stashes: ${Number(preflight.stashCount ?? 0)}`,
|
|
13
|
+
];
|
|
14
|
+
const reason = textValue(result.reason, "");
|
|
15
|
+
if (result.ok === false || reason) lines.push(`[FAIL] ${reason || "guardian_finish_workflow blocked"}`);
|
|
16
|
+
if (typeof result.confirmToken === "string") lines.push(`[WARN] confirmToken: ${result.confirmToken}`);
|
|
17
|
+
if (candidates.length > 0) {
|
|
18
|
+
lines.push("[INFO] cleanup candidates:");
|
|
19
|
+
for (const entry of candidates.slice(0, 8)) {
|
|
20
|
+
const candidate = recordValue(entry);
|
|
21
|
+
lines.push(` - kind=${textValue(candidate.kind)} targetKind=${textValue(candidate.targetKind)} branch=${textValue(candidate.branch)} path=${textValue(candidate.targetPath)} head=${shortCommit(candidate.head)}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (blockers.length > 0) {
|
|
25
|
+
lines.push("[WARN] cleanup blockers:");
|
|
26
|
+
for (const entry of blockers.slice(0, 8)) {
|
|
27
|
+
const blocker = recordValue(entry);
|
|
28
|
+
lines.push(` - kind=${textValue(blocker.kind)} branch=${textValue(blocker.branch)} path=${textValue(blocker.targetPath)} reason=${textValue(blocker.reason)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (results.length > 0) {
|
|
32
|
+
lines.push("[INFO] cleanup results:");
|
|
33
|
+
for (const entry of results.slice(0, 8)) {
|
|
34
|
+
const item = recordValue(entry);
|
|
35
|
+
lines.push(` - status=${textValue(item.status)} branch=${textValue(item.branch)} worktreeRemoved=${String(item.worktreeRemoved === true)} branchDeleted=${String(item.branchDeleted === true)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatGuardianDoneOutput(rawResult: unknown) {
|
|
42
|
+
const result = recordValue(rawResult);
|
|
43
|
+
const preflight = recordValue(result.preflight);
|
|
44
|
+
const cleanupPlan = recordValue(result.cleanupPlan);
|
|
45
|
+
const dirtySnapshot = recordValue(result.dirtySnapshot);
|
|
46
|
+
const dirtyPaths = arrayValue(dirtySnapshot.paths ?? preflight.dirtyFiles);
|
|
47
|
+
const lines = [
|
|
48
|
+
`${result.ok === false ? "[FAIL]" : result.status === "planned" ? "[WARN]" : "[GOOD]"} guardian_done ${textValue(result.status)}`,
|
|
49
|
+
`[INFO] lane: ${textValue(result.lane)} | branch: ${textValue(preflight.currentBranch ?? result.branch)} | baseBranch: ${textValue(preflight.baseBranch)}`,
|
|
50
|
+
`[INFO] dirty: ${dirtyPaths.length} | stashes: ${Number(preflight.stashCount ?? 0)} | safetyRef: ${textValue(result.safetyRef)}`,
|
|
51
|
+
];
|
|
52
|
+
const reason = textValue(result.reason, "");
|
|
53
|
+
if (result.ok === false || reason) lines.push(`[FAIL] ${reason || "guardian_done blocked"}`);
|
|
54
|
+
if (typeof result.nextAction === "string") lines.push(`[INFO] nextAction: ${result.nextAction}`);
|
|
55
|
+
if (typeof result.commitMessage === "string") lines.push(`[INFO] commitMessage: ${result.commitMessage}`);
|
|
56
|
+
if (typeof result.commit === "string") lines.push(`[INFO] commit: ${shortCommit(result.commit)}`);
|
|
57
|
+
if (dirtyPaths.length > 0) {
|
|
58
|
+
lines.push("[INFO] dirty files:");
|
|
59
|
+
for (const entry of dirtyPaths.slice(0, 8)) lines.push(` - ${textValue(entry, String(entry))}`);
|
|
60
|
+
}
|
|
61
|
+
if (Object.keys(cleanupPlan).length > 0) {
|
|
62
|
+
lines.push(`[INFO] cleanupPlan: ${textValue(cleanupPlan.status)} candidates=${arrayValue(cleanupPlan.candidates).length} blockers=${arrayValue(cleanupPlan.blockers).length}`);
|
|
63
|
+
}
|
|
64
|
+
const suggestions = arrayValue(result.suggestedCommands);
|
|
65
|
+
if (suggestions.length > 0) {
|
|
66
|
+
lines.push("[INFO] suggested commands:");
|
|
67
|
+
for (const command of suggestions.slice(0, 8)) lines.push(` - ${textValue(command, String(command))}`);
|
|
68
|
+
}
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { formatGuardianDeleteOutput, formatGuardianDeletePathsOutput, formatGuardianHygieneOutput, formatGuardianUnblockFinishOutput } from "./readable-output-cleanup.ts";
|
|
2
|
+
import { formatGuardianReportOutput, formatGuardianStatusOutput } from "./readable-output-status.ts";
|
|
3
|
+
import { formatGuardianDoneOutput, formatGuardianFinishWorkflowOutput } from "./readable-output-workflow.ts";
|
|
4
|
+
|
|
5
|
+
export const READABLE_GUARDIAN_TOOLS = new Set(["guardian_status", "guardian_recover", "guardian_report_html", "guardian_hygiene", "guardian_delete_paths", "guardian_delete_worktree", "guardian_unblock_finish", "guardian_finish_workflow", "guardian_done"]);
|
|
6
|
+
|
|
7
|
+
export function formatGuardianOutput(name: string, result: unknown) {
|
|
8
|
+
if (name === "guardian_report_html") return formatGuardianReportOutput(result);
|
|
9
|
+
if (name === "guardian_hygiene") return formatGuardianHygieneOutput(result);
|
|
10
|
+
if (name === "guardian_delete_paths") return formatGuardianDeletePathsOutput(result);
|
|
11
|
+
if (name === "guardian_delete_worktree") return formatGuardianDeleteOutput(result);
|
|
12
|
+
if (name === "guardian_unblock_finish") return formatGuardianUnblockFinishOutput(result);
|
|
13
|
+
if (name === "guardian_finish_workflow") return formatGuardianFinishWorkflowOutput(result);
|
|
14
|
+
if (name === "guardian_done") return formatGuardianDoneOutput(result);
|
|
15
|
+
return formatGuardianStatusOutput(name, result);
|
|
16
|
+
}
|