@sayknow-cli/coding-agent 0.2.4 → 0.2.5
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/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/cli/update-cli.d.ts +3 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/config/keybindings.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +27 -0
- package/dist/types/harness-control-plane/storage.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +12 -4
- package/dist/types/lsp/startup-events.d.ts +1 -0
- package/dist/types/migrate/action-planner.d.ts +11 -0
- package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
- package/dist/types/migrate/adapters/codex.d.ts +5 -0
- package/dist/types/migrate/adapters/index.d.ts +45 -0
- package/dist/types/migrate/adapters/opencode.d.ts +2 -0
- package/dist/types/migrate/executor.d.ts +2 -0
- package/dist/types/migrate/mcp-mapper.d.ts +20 -0
- package/dist/types/migrate/report.d.ts +18 -0
- package/dist/types/migrate/skill-normalizer.d.ts +27 -0
- package/dist/types/migrate/types.d.ts +126 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
- package/dist/types/research-plan/index.d.ts +1 -0
- package/dist/types/research-plan/ledger.d.ts +33 -0
- package/dist/types/rlm/artifacts.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
- package/dist/types/skc-runtime/deep-interview-recorder.d.ts +2 -0
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +2 -2
- package/dist/types/skc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/skc-runtime/session-layout.d.ts +59 -0
- package/dist/types/skc-runtime/session-resolution.d.ts +47 -0
- package/dist/types/skc-runtime/state-graph.d.ts +1 -1
- package/dist/types/skc-runtime/state-runtime.d.ts +5 -4
- package/dist/types/skc-runtime/state-schema.d.ts +2 -0
- package/dist/types/skc-runtime/state-writer.d.ts +36 -7
- package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +7 -4
- package/dist/types/skc-runtime/workflow-command-ref.d.ts +1 -1
- package/dist/types/skc-runtime/workflow-manifest.d.ts +1 -1
- package/dist/types/skill-state/active-state.d.ts +6 -11
- package/dist/types/skill-state/canonical-skills.d.ts +3 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +5 -0
- package/dist/types/skill-state/workflow-hud.d.ts +2 -0
- package/dist/types/task/spawn-gate.d.ts +1 -10
- package/package.json +7 -7
- package/scripts/build-binary.ts +0 -7
- package/src/cli/migrate-cli.ts +106 -0
- package/src/cli/setup-cli.ts +14 -1
- package/src/cli/update-cli.ts +53 -3
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +2 -2
- package/src/commands/launch.ts +1 -1
- package/src/commands/migrate.ts +46 -0
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +7 -3
- package/src/config/model-registry.ts +9 -2
- package/src/config/model-resolver.ts +13 -2
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +17 -0
- package/src/coordinator-mcp/policy.ts +10 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +0 -1
- package/src/defaults/skc/skills/deep-interview/SKILL.md +30 -24
- package/src/defaults/skc/skills/ralplan/SKILL.md +10 -4
- package/src/defaults/skc/skills/team/SKILL.md +51 -47
- package/src/defaults/skc/skills/ultragoal/SKILL.md +17 -13
- package/src/exec/bash-executor.ts +3 -1
- package/src/extensibility/custom-commands/loader.ts +0 -7
- package/src/extensibility/skc-plugins/injection.ts +23 -4
- package/src/extensibility/skc-plugins/state.ts +16 -1
- package/src/harness-control-plane/storage.ts +14 -4
- package/src/hooks/native-skill-hook.ts +38 -12
- package/src/hooks/skill-state.ts +178 -83
- package/src/internal-urls/docs-index.generated.ts +11 -9
- package/src/lsp/startup-events.ts +24 -0
- package/src/migrate/action-planner.ts +318 -0
- package/src/migrate/adapters/claude-code.ts +39 -0
- package/src/migrate/adapters/codex.ts +70 -0
- package/src/migrate/adapters/index.ts +277 -0
- package/src/migrate/adapters/opencode.ts +52 -0
- package/src/migrate/executor.ts +81 -0
- package/src/migrate/mcp-mapper.ts +152 -0
- package/src/migrate/report.ts +104 -0
- package/src/migrate/skill-normalizer.ts +80 -0
- package/src/migrate/types.ts +163 -0
- package/src/modes/bridge/bridge-mode.ts +2 -2
- package/src/modes/components/custom-editor.ts +30 -20
- package/src/modes/components/welcome.ts +20 -9
- package/src/modes/controllers/input-controller.ts +21 -3
- package/src/modes/interactive-mode.ts +27 -19
- package/src/modes/prompt-action-autocomplete.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -2
- package/src/research-plan/index.ts +1 -0
- package/src/research-plan/ledger.ts +177 -0
- package/src/rlm/artifacts.ts +12 -3
- package/src/rlm/index.ts +7 -0
- package/src/runtime-mcp/config-writer.ts +46 -0
- package/src/session/agent-session.ts +43 -41
- package/src/session/session-manager.ts +19 -2
- package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
- package/src/setup/hermes-setup.ts +1 -1
- package/src/skc-runtime/deep-interview-recorder.ts +51 -18
- package/src/skc-runtime/deep-interview-runtime.ts +49 -23
- package/src/skc-runtime/goal-mode-request.ts +26 -11
- package/src/skc-runtime/launch-tmux.ts +68 -15
- package/src/skc-runtime/ralplan-runtime.ts +79 -50
- package/src/skc-runtime/session-layout.ts +180 -0
- package/src/skc-runtime/session-resolution.ts +217 -0
- package/src/skc-runtime/state-graph.ts +1 -2
- package/src/skc-runtime/state-migrations.ts +1 -0
- package/src/skc-runtime/state-runtime.ts +237 -114
- package/src/skc-runtime/state-schema.ts +2 -0
- package/src/skc-runtime/state-writer.ts +310 -42
- package/src/skc-runtime/team-runtime.ts +43 -19
- package/src/skc-runtime/tmux-sessions.ts +43 -2
- package/src/skc-runtime/ultragoal-guard.ts +45 -2
- package/src/skc-runtime/ultragoal-runtime.ts +121 -41
- package/src/skc-runtime/workflow-command-ref.ts +1 -2
- package/src/skc-runtime/workflow-manifest.ts +1 -2
- package/src/skill-state/active-state.ts +116 -129
- package/src/skill-state/canonical-skills.ts +4 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +238 -111
- package/src/skill-state/workflow-hud.ts +4 -2
- package/src/skill-state/workflow-state-contract.ts +3 -3
- package/src/slash-commands/builtin-registry.ts +8 -4
- package/src/system-prompt.ts +11 -9
- package/src/task/agents.ts +1 -22
- package/src/task/index.ts +1 -41
- package/src/task/spawn-gate.ts +1 -38
- package/src/task/types.ts +1 -1
- package/src/tools/ask.ts +34 -12
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/computer.ts +58 -4
- package/src/utils/edit-mode.ts +1 -1
- package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
- package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
- package/src/prompts/agents/explore.md +0 -58
- package/src/prompts/agents/plan.md +0 -49
- package/src/prompts/agents/reviewer.md +0 -141
- package/src/prompts/agents/task.md +0 -16
- package/src/prompts/review-request.md +0 -70
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
1
3
|
import * as path from "node:path";
|
|
2
4
|
import type { AgentTool } from "@sayknow-cli/agent-core";
|
|
3
5
|
import { logger } from "@sayknow-cli/utils";
|
|
4
6
|
import { expandApplyPatchToEntries } from "../edit/modes/apply-patch";
|
|
5
7
|
import { LocalProtocolHandler, resolveLocalUrlToPath } from "../internal-urls/local-protocol";
|
|
8
|
+
import { SKC_SESSION_PREFIX, modeStatePath as sessionModeStatePath } from "../skc-runtime/session-layout";
|
|
9
|
+
import { resolveSkcSessionForRead } from "../skc-runtime/session-resolution";
|
|
6
10
|
import { ModeStateSchema } from "../skc-runtime/state-schema";
|
|
7
11
|
import { resolveToCwd } from "../tools/path-utils";
|
|
8
12
|
import { ToolError } from "../tools/tool-errors";
|
|
@@ -17,15 +21,22 @@ export const DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE =
|
|
|
17
21
|
"Deep-interview phase boundary: continue gathering context/questions/risks and emit a handoff/spec before code edits. Mutation tools and patch execution are blocked while deep-interview is active; finalize specs through `skc deep-interview --write --stage final` or hand off to an execution phase.";
|
|
18
22
|
export const WORKFLOW_STATE_MUTATION_BLOCK_MESSAGE =
|
|
19
23
|
".skc workflow state and artifacts are runtime-owned. Agent mutation tools cannot edit `.skc/**`; use the sanctioned `skc` CLI instead.";
|
|
24
|
+
export const RALPLAN_MUTATION_BLOCK_MESSAGE =
|
|
25
|
+
"Ralplan planning phase boundary: keep refining the consensus plan and persist plan artifacts through `skc ralplan --write` (stage scratch files under a temp dir if needed). Product-code mutation tools and patch execution are blocked while ralplan is active; mutate only after the plan is approved and execution begins.";
|
|
26
|
+
export const ULTRAGOAL_GOAL_PLANNING_MUTATION_BLOCK_MESSAGE =
|
|
27
|
+
"Ultragoal goal-planning phase boundary: finish goal planning and record goals through `skc ultragoal` before editing code. Product-code mutation tools and patch execution are blocked until goal planning completes and execution begins.";
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
/** Resolve the phase-boundary block message for the active planning skill. */
|
|
30
|
+
function planningPhaseBlockMessage(skill: CanonicalSkcWorkflowSkill): string {
|
|
31
|
+
if (skill === "ralplan") return RALPLAN_MUTATION_BLOCK_MESSAGE;
|
|
32
|
+
if (skill === "ultragoal") return ULTRAGOAL_GOAL_PLANNING_MUTATION_BLOCK_MESSAGE;
|
|
33
|
+
return DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit"]);
|
|
22
37
|
const ARCHIVE_OR_SQLITE_BASE_RE = /^(.+?\.(?:tar\.gz|sqlite3|sqlite|db3|zip|tgz|tar|db))(?:$|:)/i;
|
|
23
38
|
const INTERNAL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
24
39
|
const VIM_FILE_SWITCH_RE = /^\s*:(?:e|e!|edit|edit!)(?:\s+([^<\r\n]+))?(?:<CR>|\r|\n|$)/i;
|
|
25
|
-
const BASH_TOKEN_RE = /'[^']*'|"(?:\\.|[^"\\])*"|\S+/g;
|
|
26
|
-
const BASH_REDIRECT_RE = /^(?:\d*)>>?$/;
|
|
27
|
-
const BASH_HEREDOC_RE = /^(?:\d*)<<-?$/;
|
|
28
|
-
const BASH_MUTATION_COMMANDS = new Set(["rm", "mv", "cp", "touch", "mkdir", "ln", "tee"]);
|
|
29
40
|
|
|
30
41
|
type ToolWithEditMode = AgentTool & {
|
|
31
42
|
mode?: unknown;
|
|
@@ -67,15 +78,18 @@ function safeString(value: unknown): string {
|
|
|
67
78
|
return typeof value === "string" ? value : "";
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
function
|
|
71
|
-
|
|
81
|
+
async function resolveBoundarySessionId(cwd: string, sessionId?: string): Promise<string | null> {
|
|
82
|
+
const normalizedSessionId = sessionId?.trim();
|
|
83
|
+
if (normalizedSessionId) return normalizedSessionId;
|
|
84
|
+
try {
|
|
85
|
+
return (await resolveSkcSessionForRead(cwd, { envSessionId: process.env.SKC_SESSION_ID })).skcSessionId;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
72
89
|
}
|
|
73
90
|
|
|
74
|
-
function modeStatePath(cwd: string, skill: string, sessionId
|
|
75
|
-
|
|
76
|
-
const fileName = `${skill}-state.json`;
|
|
77
|
-
if (sessionId) return path.join(stateDir, "sessions", encodePathSegment(sessionId), fileName);
|
|
78
|
-
return path.join(stateDir, fileName);
|
|
91
|
+
function modeStatePath(cwd: string, skill: string, sessionId: string): string {
|
|
92
|
+
return sessionModeStatePath(cwd, sessionId, skill);
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
function warnInvalidModeState(filePath: string, error: string): void {
|
|
@@ -103,21 +117,17 @@ async function readValidatedModeState(filePath: string): Promise<ModeState | nul
|
|
|
103
117
|
}
|
|
104
118
|
return state;
|
|
105
119
|
}
|
|
106
|
-
async function readVisibleModeState(cwd: string, skill: string, sessionId
|
|
107
|
-
|
|
108
|
-
const sessionState = await readValidatedModeState(modeStatePath(cwd, skill, sessionId));
|
|
109
|
-
if (sessionState) return sessionState;
|
|
110
|
-
}
|
|
111
|
-
return await readValidatedModeState(modeStatePath(cwd, skill));
|
|
120
|
+
async function readVisibleModeState(cwd: string, skill: string, sessionId: string): Promise<ModeState | null> {
|
|
121
|
+
return await readValidatedModeState(modeStatePath(cwd, skill, sessionId));
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Phases that genuinely finish a workflow skill. Mirrors the Stop hook's
|
|
126
|
+
* `STOP_RELEASING_PHASES` (`hooks/skill-state.ts`): `handoff` is intentionally
|
|
127
|
+
* absent so a handoff-required planning skill (deep-interview/ralplan) keeps
|
|
128
|
+
* blocking through its handoff/ask window until it is demoted or cleared.
|
|
129
|
+
*/
|
|
130
|
+
const WORKFLOW_FINISHED_PHASES = new Set(["complete", "completed", "failed", "cancelled", "canceled", "inactive"]);
|
|
121
131
|
|
|
122
132
|
function entryMatchesContext(entry: SkillActiveEntry, sessionId?: string, threadId?: string): boolean {
|
|
123
133
|
if (sessionId && entry.session_id && entry.session_id !== sessionId) return false;
|
|
@@ -131,17 +141,94 @@ function modeStateMatchesContext(state: ModeState, sessionId?: string, threadId?
|
|
|
131
141
|
return true;
|
|
132
142
|
}
|
|
133
143
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
144
|
+
/** Workflow skills that have a pre-approval planning posture this guard enforces. `team` never does. */
|
|
145
|
+
function isPlanningSkill(skill: string): skill is "deep-interview" | "ralplan" | "ultragoal" {
|
|
146
|
+
return skill === "deep-interview" || skill === "ralplan" || skill === "ultragoal";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Whether `skill` in `phase` is a pre-approval planning posture that must block
|
|
151
|
+
* product-code mutation. `deep-interview` and `ralplan` are wholly pre-approval
|
|
152
|
+
* (every phase blocks except a genuinely-finished one — `handoff` and ralplan's
|
|
153
|
+
* `final` keep blocking until execution is approved and the skill is demoted).
|
|
154
|
+
* `ultragoal` only blocks during `goal-planning`; once goals are created it is an
|
|
155
|
+
* executor and mutates freely.
|
|
156
|
+
*/
|
|
157
|
+
function isBlockingPlanningPhase(skill: "deep-interview" | "ralplan" | "ultragoal", phase: string): boolean {
|
|
158
|
+
const normalized = phase.trim().toLowerCase();
|
|
159
|
+
if (skill === "ultragoal") return normalized === "goal-planning";
|
|
160
|
+
return !WORKFLOW_FINISHED_PHASES.has(normalized);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface ActivePlanningSkill {
|
|
164
|
+
skill: "deep-interview" | "ralplan" | "ultragoal";
|
|
165
|
+
phase: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Pick the single CURRENT workflow entry among active entries.
|
|
170
|
+
*
|
|
171
|
+
* Steady state has exactly one active workflow skill (handoff demotes the prior
|
|
172
|
+
* to `active:false`, which `listActiveSkills` already filters out). If several
|
|
173
|
+
* are momentarily active, prefer the most-recently-updated entry so a stale
|
|
174
|
+
* planning row (e.g. a still-active ralplan `final`) can never be selected over a
|
|
175
|
+
* newer executor (ultragoal/team), and a planning *return* (newer `updated_at`)
|
|
176
|
+
* reliably wins. Ties fall back to the resolved top-level `skill`, then to the
|
|
177
|
+
* first entry, matching how the HUD/chain guard pick `activeSkills[0]`.
|
|
178
|
+
*/
|
|
179
|
+
function resolveCurrentWorkflowEntry(entries: SkillActiveEntry[], topLevelSkill: string): SkillActiveEntry {
|
|
180
|
+
const ts = (entry: SkillActiveEntry): number => {
|
|
181
|
+
const value = Date.parse(safeString(entry.updated_at) || safeString(entry.activated_at));
|
|
182
|
+
return Number.isNaN(value) ? -1 : value;
|
|
183
|
+
};
|
|
184
|
+
let best = entries[0];
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
const delta = ts(entry) - ts(best);
|
|
187
|
+
if (delta > 0) best = entry;
|
|
188
|
+
else if (delta === 0 && topLevelSkill && entry.skill === topLevelSkill) best = entry;
|
|
189
|
+
}
|
|
190
|
+
return best;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Resolve the single active pre-approval planning skill for this context, or null.
|
|
195
|
+
*
|
|
196
|
+
* Transition/return safety: this keys off the ONE canonical current workflow
|
|
197
|
+
* skill (the resolved top-level `skill` that the HUD and the skill-tool chain
|
|
198
|
+
* guard treat as active), not an independent scan of every skill. A handoff
|
|
199
|
+
* atomically demotes the prior skill and promotes the callee, and a return
|
|
200
|
+
* (e.g. re-entering ralplan/deep-interview after an ultragoal goal completes)
|
|
201
|
+
* re-activates the planning skill — in every case "whatever skill is current"
|
|
202
|
+
* governs, so a stale planning entry can never block while an executor runs and
|
|
203
|
+
* a resumed planning phase reliably re-blocks.
|
|
204
|
+
*
|
|
205
|
+
* Fail-open contract: a missing or invalid durable mode-state releases the block
|
|
206
|
+
* (a corrupt state file must not lock all mutation), matching the guard's
|
|
207
|
+
* historical behavior — this is intentionally looser than the Stop hook, which
|
|
208
|
+
* fails closed for handoff-required skills.
|
|
209
|
+
*/
|
|
210
|
+
async function getActivePlanningSkill(
|
|
211
|
+
cwd: string,
|
|
212
|
+
sessionId?: string,
|
|
213
|
+
threadId?: string,
|
|
214
|
+
): Promise<ActivePlanningSkill | null> {
|
|
215
|
+
const resolvedSessionId = await resolveBoundarySessionId(cwd, sessionId);
|
|
216
|
+
if (!resolvedSessionId) return null;
|
|
217
|
+
const skillState = await readVisibleSkillActiveState(cwd, resolvedSessionId);
|
|
218
|
+
if (!skillState) return null;
|
|
219
|
+
const activeEntries = listActiveSkills(skillState).filter(entry =>
|
|
220
|
+
entryMatchesContext(entry, resolvedSessionId, threadId),
|
|
138
221
|
);
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
return
|
|
222
|
+
if (activeEntries.length === 0) return null;
|
|
223
|
+
const current = resolveCurrentWorkflowEntry(activeEntries, safeString(skillState.skill).trim());
|
|
224
|
+
if (!isPlanningSkill(current.skill)) return null;
|
|
225
|
+
const modeState = await readVisibleModeState(cwd, current.skill, resolvedSessionId);
|
|
226
|
+
if (!modeState) return null;
|
|
227
|
+
if (modeState.active !== true) return null;
|
|
228
|
+
if (!modeStateMatchesContext(modeState, resolvedSessionId, threadId)) return null;
|
|
229
|
+
const phase = String(modeState.current_phase ?? current.phase ?? "").trim();
|
|
230
|
+
if (!isBlockingPlanningPhase(current.skill, phase)) return null;
|
|
231
|
+
return { skill: current.skill, phase };
|
|
145
232
|
}
|
|
146
233
|
|
|
147
234
|
function normalizePosix(value: string): string {
|
|
@@ -243,75 +330,10 @@ function extractEditTargets(args: unknown, tool: ToolWithEditMode): ExtractedTar
|
|
|
243
330
|
return targets;
|
|
244
331
|
}
|
|
245
332
|
|
|
246
|
-
function extractBashTargets(args: unknown): ExtractedTargets {
|
|
247
|
-
const record = getRecord(args);
|
|
248
|
-
const command = safeString(record?.command).trim();
|
|
249
|
-
const targets: ExtractedTargets = { paths: [], unknown: false };
|
|
250
|
-
if (!command) {
|
|
251
|
-
targets.unknown = true;
|
|
252
|
-
return targets;
|
|
253
|
-
}
|
|
254
|
-
if (/^skc(?:\s|$)/.test(command)) return targets;
|
|
255
|
-
|
|
256
|
-
const tokens = command.match(BASH_TOKEN_RE)?.map(unquoteBashToken) ?? [];
|
|
257
|
-
for (let index = 0; index < tokens.length; index++) {
|
|
258
|
-
const token = tokens[index] ?? "";
|
|
259
|
-
if (BASH_REDIRECT_RE.test(token)) {
|
|
260
|
-
addPath(targets, tokens[index + 1]);
|
|
261
|
-
index++;
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
const redirectMatch = token.match(/^(?:\d*)>>?(.+)$/);
|
|
265
|
-
if (redirectMatch?.[1]) {
|
|
266
|
-
addPath(targets, redirectMatch[1]);
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
if (BASH_HEREDOC_RE.test(token)) {
|
|
270
|
-
addPath(targets, tokens[index + 1]);
|
|
271
|
-
index++;
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
const heredocMatch = token.match(/^(?:\d*)<<-?(.+)$/);
|
|
275
|
-
if (heredocMatch?.[1]) {
|
|
276
|
-
addPath(targets, heredocMatch[1]);
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
if (isMutationBashCommand(tokens, index)) {
|
|
280
|
-
for (let targetIndex = index + 1; targetIndex < tokens.length; targetIndex++) {
|
|
281
|
-
const target = tokens[targetIndex] ?? "";
|
|
282
|
-
if (isBashCommandBoundary(target)) break;
|
|
283
|
-
if (target.startsWith("-")) continue;
|
|
284
|
-
addPath(targets, target);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return targets;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function unquoteBashToken(token: string): string {
|
|
292
|
-
if (token.length < 2) return token;
|
|
293
|
-
const quote = token[0];
|
|
294
|
-
if ((quote === "'" || quote === '"') && token.at(-1) === quote) return token.slice(1, -1);
|
|
295
|
-
return token;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function isBashCommandBoundary(token: string): boolean {
|
|
299
|
-
return [";", "&&", "||", "|"].includes(token);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function isMutationBashCommand(tokens: string[], index: number): boolean {
|
|
303
|
-
const token = path.basename(tokens[index] ?? "");
|
|
304
|
-
if (BASH_MUTATION_COMMANDS.has(token)) return true;
|
|
305
|
-
if (token !== "sed") return false;
|
|
306
|
-
const next = tokens[index + 1] ?? "";
|
|
307
|
-
return next === "-i" || next.startsWith("-i") || next.includes("i");
|
|
308
|
-
}
|
|
309
|
-
|
|
310
333
|
function extractTargets(tool: ToolWithEditMode, args: unknown): ExtractedTargets {
|
|
311
334
|
if (tool.name === "write") return extractWriteTargets(args);
|
|
312
335
|
if (tool.name === "ast_edit") return extractAstEditTargets(args);
|
|
313
336
|
if (tool.name === "edit") return extractEditTargets(args, tool);
|
|
314
|
-
if (tool.name === "bash") return extractBashTargets(args);
|
|
315
337
|
return { paths: [], unknown: true };
|
|
316
338
|
}
|
|
317
339
|
|
|
@@ -355,8 +377,9 @@ function relativeSkcSegments(cwd: string, rawPath: string): string[] | null {
|
|
|
355
377
|
function blockedWorkflowStateSkill(cwd: string, rawPath: string): CanonicalSkcWorkflowSkill | null {
|
|
356
378
|
const segments = relativeSkcSegments(cwd, rawPath);
|
|
357
379
|
if (segments?.[0] !== ".skc") return null;
|
|
358
|
-
|
|
359
|
-
if (
|
|
380
|
+
const generatedRoot = segments[1]?.startsWith(SKC_SESSION_PREFIX) ? segments[2] : segments[1];
|
|
381
|
+
if (generatedRoot === "specs" || generatedRoot === "plans") return null;
|
|
382
|
+
if (generatedRoot !== "state") return null;
|
|
360
383
|
const fileName = segments.at(-1) ?? "";
|
|
361
384
|
for (const skillName of ["deep-interview", "ralplan", "ultragoal", "team"] as const) {
|
|
362
385
|
if (fileName === workflowModeStateFileName(skillName)) return skillName;
|
|
@@ -376,7 +399,8 @@ function firstBlockedWorkflowStateSkill(cwd: string, targets: ExtractedTargets):
|
|
|
376
399
|
function isAllowlistedPath(cwd: string, rawPath: string): boolean {
|
|
377
400
|
const segments = relativeSkcSegments(cwd, rawPath);
|
|
378
401
|
if (segments?.[0] !== ".skc") return false;
|
|
379
|
-
|
|
402
|
+
const generatedRoot = segments[1]?.startsWith(SKC_SESSION_PREFIX) ? segments[2] : segments[1];
|
|
403
|
+
return generatedRoot === "specs" || generatedRoot === "plans";
|
|
380
404
|
}
|
|
381
405
|
function isBlockedSkcPath(cwd: string, rawPath: string): boolean {
|
|
382
406
|
const segments = relativeSkcSegments(cwd, rawPath);
|
|
@@ -392,6 +416,84 @@ function allTargetsAllowlisted(cwd: string, targets: ExtractedTargets): boolean
|
|
|
392
416
|
!targets.unknown && targets.paths.length > 0 && targets.paths.every(rawPath => isAllowlistedPath(cwd, rawPath))
|
|
393
417
|
);
|
|
394
418
|
}
|
|
419
|
+
|
|
420
|
+
function neutralTempRoots(): string[] {
|
|
421
|
+
const roots = new Set<string>();
|
|
422
|
+
const add = (value: string | undefined): void => {
|
|
423
|
+
const trimmed = value?.trim();
|
|
424
|
+
if (trimmed) roots.add(path.resolve(trimmed));
|
|
425
|
+
};
|
|
426
|
+
add(os.tmpdir());
|
|
427
|
+
add(process.env.TMPDIR);
|
|
428
|
+
for (const fixed of ["/tmp", "/var/tmp", "/private/tmp", "/private/var/tmp"]) add(fixed);
|
|
429
|
+
return [...roots];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function isPathWithin(root: string, target: string): boolean {
|
|
433
|
+
const rel = path.relative(root, target);
|
|
434
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function realpathOrSelf(target: string): Promise<string> {
|
|
438
|
+
try {
|
|
439
|
+
return await fs.realpath(target);
|
|
440
|
+
} catch {
|
|
441
|
+
return target;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Canonicalize a target whose leaf may not exist yet (we are about to write it):
|
|
447
|
+
* realpath the nearest existing ancestor and re-append the not-yet-existing
|
|
448
|
+
* suffix, so a symlinked ancestor (or macOS `/tmp` → `/private/tmp` alias) is
|
|
449
|
+
* resolved to its real location.
|
|
450
|
+
*/
|
|
451
|
+
async function canonicalizeForContainment(absolutePath: string): Promise<string> {
|
|
452
|
+
const suffix: string[] = [];
|
|
453
|
+
let current = absolutePath;
|
|
454
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
455
|
+
try {
|
|
456
|
+
const real = await fs.realpath(current);
|
|
457
|
+
return suffix.length > 0 ? path.join(real, ...suffix.reverse()) : real;
|
|
458
|
+
} catch {
|
|
459
|
+
const parent = path.dirname(current);
|
|
460
|
+
if (parent === current) break;
|
|
461
|
+
suffix.push(path.basename(current));
|
|
462
|
+
current = parent;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return absolutePath;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* A neutral scratch path the planning-phase block tolerates: it resolves to a
|
|
470
|
+
* system temp directory and lives OUTSIDE the project cwd. Files inside the
|
|
471
|
+
* project tree (product code, `.skc/**`) are never neutral, even when the cwd
|
|
472
|
+
* itself is rooted under a temp dir. The lexical checks run first; a canonical
|
|
473
|
+
* (symlink/alias-resolved) re-check then ensures the REAL target is still outside
|
|
474
|
+
* the project and inside a real temp root, defeating a temp symlink that points
|
|
475
|
+
* back into the repo or `.skc/`.
|
|
476
|
+
*/
|
|
477
|
+
async function isNeutralTempPath(cwd: string, rawPath: string): Promise<boolean> {
|
|
478
|
+
const { absolutePath, unknown } = resolveRawPath(cwd, rawPath);
|
|
479
|
+
if (unknown || !absolutePath) return false;
|
|
480
|
+
const resolvedCwd = path.resolve(cwd);
|
|
481
|
+
if (isPathWithin(resolvedCwd, absolutePath)) return false;
|
|
482
|
+
if (!neutralTempRoots().some(root => isPathWithin(root, absolutePath))) return false;
|
|
483
|
+
const realTarget = await canonicalizeForContainment(absolutePath);
|
|
484
|
+
if (isPathWithin(await realpathOrSelf(resolvedCwd), realTarget)) return false;
|
|
485
|
+
const realRoots = await Promise.all(neutralTempRoots().map(realpathOrSelf));
|
|
486
|
+
return realRoots.some(root => isPathWithin(root, realTarget));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Targets that remain disallowed during a planning phase (excludes neutral temp scratch). */
|
|
490
|
+
async function planningBlockedTargets(cwd: string, targets: ExtractedTargets): Promise<string[]> {
|
|
491
|
+
const blocked: string[] = [];
|
|
492
|
+
for (const rawPath of targets.paths) {
|
|
493
|
+
if (!(await isNeutralTempPath(cwd, rawPath))) blocked.push(rawPath);
|
|
494
|
+
}
|
|
495
|
+
return blocked;
|
|
496
|
+
}
|
|
395
497
|
export async function assertDeepInterviewMutationRawPathsAllowed(input: {
|
|
396
498
|
cwd: string;
|
|
397
499
|
sessionId?: string;
|
|
@@ -399,12 +501,21 @@ export async function assertDeepInterviewMutationRawPathsAllowed(input: {
|
|
|
399
501
|
rawPaths: string[];
|
|
400
502
|
forceOverride?: boolean;
|
|
401
503
|
}): Promise<void> {
|
|
402
|
-
if (input.forceOverride) return;
|
|
403
|
-
if (!(await isActiveDeepInterview(input.cwd, input.sessionId, input.threadId))) return;
|
|
404
504
|
const targets: ExtractedTargets = { paths: input.rawPaths, unknown: input.rawPaths.length === 0 };
|
|
405
|
-
|
|
406
|
-
|
|
505
|
+
// Always-on `.skc/**` runtime-owned block, in parity with getDeepInterviewMutationDecision
|
|
506
|
+
// and ahead of forceOverride: a deferred ast_edit apply must not reach `.skc/**` either.
|
|
507
|
+
if (hasBlockedSkcTarget(input.cwd, targets)) {
|
|
508
|
+
const stateSkill = firstBlockedWorkflowStateSkill(input.cwd, targets);
|
|
509
|
+
const command = stateSkill ? sanctionedWorkflowStateCommand(stateSkill) : "skc <workflow-command>";
|
|
510
|
+
throw new ToolError(`${WORKFLOW_STATE_MUTATION_BLOCK_MESSAGE}\nUse: ${command}`);
|
|
407
511
|
}
|
|
512
|
+
if (input.forceOverride) return;
|
|
513
|
+
const planning = await getActivePlanningSkill(input.cwd, input.sessionId, input.threadId);
|
|
514
|
+
if (!planning) return;
|
|
515
|
+
const message = planningPhaseBlockMessage(planning.skill);
|
|
516
|
+
if (input.rawPaths.length === 0) throw new ToolError(message);
|
|
517
|
+
const blocked = await planningBlockedTargets(input.cwd, targets);
|
|
518
|
+
if (blocked.length > 0) throw new ToolError(message);
|
|
408
519
|
}
|
|
409
520
|
|
|
410
521
|
export async function getDeepInterviewMutationDecision(
|
|
@@ -423,24 +534,30 @@ export async function getDeepInterviewMutationDecision(
|
|
|
423
534
|
command,
|
|
424
535
|
};
|
|
425
536
|
}
|
|
426
|
-
|
|
537
|
+
const planning = await getActivePlanningSkill(input.cwd, input.sessionId, input.threadId);
|
|
538
|
+
if (!planning) {
|
|
427
539
|
return { blocked: false, targets: [] };
|
|
428
540
|
}
|
|
429
541
|
if (input.forceOverride) return { blocked: false, targets: [] };
|
|
542
|
+
const message = planningPhaseBlockMessage(planning.skill);
|
|
430
543
|
if (targets.unknown) {
|
|
431
544
|
return {
|
|
432
545
|
blocked: true,
|
|
433
|
-
message
|
|
546
|
+
message,
|
|
434
547
|
targets: targets.paths,
|
|
435
548
|
reason: "unknown-target",
|
|
436
549
|
};
|
|
437
550
|
}
|
|
438
|
-
|
|
551
|
+
// Neutral temp scratch (outside the project tree) stays writable so agents can
|
|
552
|
+
// stage artifacts and feed their path to the sanctioned `skc ... --write` CLIs.
|
|
553
|
+
// Read-only / `skc` bash extract no targets and fall through to allowed here.
|
|
554
|
+
const blockedTargets = await planningBlockedTargets(input.cwd, targets);
|
|
555
|
+
if (blockedTargets.length === 0) {
|
|
439
556
|
return { blocked: false, targets: targets.paths };
|
|
440
557
|
}
|
|
441
558
|
return {
|
|
442
559
|
blocked: true,
|
|
443
|
-
message
|
|
560
|
+
message,
|
|
444
561
|
targets: targets.paths,
|
|
445
562
|
reason: allTargetsAllowlisted(input.cwd, targets) ? "handoff-artifact-tool-target" : "phase-boundary",
|
|
446
563
|
};
|
|
@@ -450,3 +567,13 @@ export async function assertDeepInterviewMutationAllowed(input: DeepInterviewMut
|
|
|
450
567
|
const decision = await getDeepInterviewMutationDecision(input);
|
|
451
568
|
if (decision.blocked) throw new ToolError(decision.message ?? DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE);
|
|
452
569
|
}
|
|
570
|
+
|
|
571
|
+
/*
|
|
572
|
+
* Generic cross-workflow names for this planning-phase mutation guard. The guard
|
|
573
|
+
* now governs deep-interview, ralplan, and ultragoal goal-planning, so new
|
|
574
|
+
* callers SHOULD use these names; the `*DeepInterview*` exports above remain as
|
|
575
|
+
* compatibility aliases (and are still what the test-suite imports).
|
|
576
|
+
*/
|
|
577
|
+
export const getWorkflowMutationDecision = getDeepInterviewMutationDecision;
|
|
578
|
+
export const assertWorkflowMutationAllowed = assertDeepInterviewMutationAllowed;
|
|
579
|
+
export const assertWorkflowMutationRawPathsAllowed = assertDeepInterviewMutationRawPathsAllowed;
|
|
@@ -40,7 +40,7 @@ interface UltragoalHudState extends WorkflowGateHudState {
|
|
|
40
40
|
currentGoal?: UltragoalLikeGoal;
|
|
41
41
|
counts: Record<string, number>;
|
|
42
42
|
goals: UltragoalLikeGoal[];
|
|
43
|
-
latestLedgerEvent?: { event?: string; goalId?: string; timestamp?: string };
|
|
43
|
+
latestLedgerEvent?: { event?: string; goalId?: string; timestamp?: string; kind?: string; evidence?: string };
|
|
44
44
|
updatedAt?: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -237,7 +237,9 @@ export function buildUltragoalHudSummary(state: UltragoalHudState): WorkflowHudS
|
|
|
237
237
|
chip(
|
|
238
238
|
"ledger",
|
|
239
239
|
state.latestLedgerEvent?.event
|
|
240
|
-
? [state.latestLedgerEvent.event, state.latestLedgerEvent.goalId]
|
|
240
|
+
? [state.latestLedgerEvent.event, state.latestLedgerEvent.kind, state.latestLedgerEvent.goalId]
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
.join(":")
|
|
241
243
|
: undefined,
|
|
242
244
|
35,
|
|
243
245
|
),
|
|
@@ -141,10 +141,10 @@ export function sanctionedWorkflowStateCommand(skill: CanonicalSkcWorkflowSkill)
|
|
|
141
141
|
export function describeWorkflowStateContract(skill: CanonicalSkcWorkflowSkill): string[] {
|
|
142
142
|
return [
|
|
143
143
|
`Sanctioned mutation path: skc state ${skill} read|write --input '<json>'`,
|
|
144
|
-
`Canonical active HUD state: .skc/
|
|
145
|
-
`Skill mode state: .skc/
|
|
144
|
+
`Canonical active HUD state: .skc/_session-{sessionid}/state/${SKILL_ACTIVE_STATE_FILE}`,
|
|
145
|
+
`Skill mode state: .skc/_session-{sessionid}/state/${workflowModeStateFileName(skill)}`,
|
|
146
146
|
"Receipts include version, skill, owner, command, state_path, storage_path, mutated_at, fresh_until, status, and mutation_id.",
|
|
147
147
|
"Receipts are fresh for 30 minutes; older receipts are stale and render as HUD warnings.",
|
|
148
|
-
"Planning artifacts under .skc/specs/** and .skc/plans/** remain writable outside the state command.",
|
|
148
|
+
"Planning artifacts under .skc/_session-{sessionid}/specs/** and .skc/_session-{sessionid}/plans/** remain writable outside the state command.",
|
|
149
149
|
];
|
|
150
150
|
}
|
|
@@ -251,11 +251,15 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
251
251
|
inlineHint: "[objective]",
|
|
252
252
|
allowArgs: true,
|
|
253
253
|
handleTui: async (command, runtime) => {
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
|
|
254
|
+
// The goal command always consumes the typed input: it either submits
|
|
255
|
+
// the bare objective (never the literal `/goal …` text the user typed)
|
|
256
|
+
// or shows a warning, so the normal submission path never records it in
|
|
257
|
+
// input history. Preserve the typed command whenever args were supplied
|
|
258
|
+
// — including the first-time `/goal set <objective>` case where goal
|
|
259
|
+
// mode was not yet active. A previous `wasGoalModeEnabled` guard dropped
|
|
260
|
+
// that first-time case from history (up/down-arrow recall).
|
|
257
261
|
await runtime.ctx.handleGoalModeCommand(command.args || undefined);
|
|
258
|
-
if (
|
|
262
|
+
if (command.args) {
|
|
259
263
|
runtime.ctx.editor.addToHistory(command.text);
|
|
260
264
|
}
|
|
261
265
|
runtime.ctx.editor.setText("");
|
package/src/system-prompt.ts
CHANGED
|
@@ -253,15 +253,17 @@ export async function loadProjectContextFiles(
|
|
|
253
253
|
|
|
254
254
|
const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
|
|
255
255
|
|
|
256
|
-
// Convert ContextFile items and preserve depth info
|
|
257
|
-
const files = result.items
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
256
|
+
// Convert project-level ContextFile items and preserve depth info
|
|
257
|
+
const files = result.items
|
|
258
|
+
.filter(item => (item as ContextFile).level === "project")
|
|
259
|
+
.map(item => {
|
|
260
|
+
const contextFile = item as ContextFile;
|
|
261
|
+
return {
|
|
262
|
+
path: contextFile.path,
|
|
263
|
+
content: contextFile.content,
|
|
264
|
+
depth: contextFile.depth,
|
|
265
|
+
};
|
|
266
|
+
});
|
|
265
267
|
|
|
266
268
|
// Sort by depth (descending): higher depth (farther from cwd) comes first,
|
|
267
269
|
// so files closer to cwd appear later and are more prominent
|
package/src/task/agents.ts
CHANGED
|
@@ -3,20 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Agents are embedded at build time via Bun's import with { type: "text" }.
|
|
5
5
|
*/
|
|
6
|
-
import { Effort } from "@sayknow-cli/ai";
|
|
7
6
|
import { parseFrontmatter, prompt } from "@sayknow-cli/utils";
|
|
8
7
|
import { parseAgentFields } from "../discovery/helpers";
|
|
8
|
+
// Embed agent markdown files at build time
|
|
9
9
|
import architectMd from "../prompts/agents/architect.md" with { type: "text" };
|
|
10
10
|
import criticMd from "../prompts/agents/critic.md" with { type: "text" };
|
|
11
11
|
import executorMd from "../prompts/agents/executor.md" with { type: "text" };
|
|
12
|
-
import exploreMd from "../prompts/agents/explore.md" with { type: "text" };
|
|
13
|
-
// Embed agent markdown files at build time
|
|
14
12
|
import agentFrontmatterTemplate from "../prompts/agents/frontmatter.md" with { type: "text" };
|
|
15
|
-
|
|
16
|
-
import planMd from "../prompts/agents/plan.md" with { type: "text" };
|
|
17
13
|
import plannerMd from "../prompts/agents/planner.md" with { type: "text" };
|
|
18
|
-
import reviewerMd from "../prompts/agents/reviewer.md" with { type: "text" };
|
|
19
|
-
import taskMd from "../prompts/agents/task.md" with { type: "text" };
|
|
20
14
|
|
|
21
15
|
import type { AgentDefinition, AgentSource } from "./types";
|
|
22
16
|
|
|
@@ -50,21 +44,6 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
|
50
44
|
{ fileName: "architect.md", template: architectMd },
|
|
51
45
|
{ fileName: "planner.md", template: plannerMd },
|
|
52
46
|
{ fileName: "critic.md", template: criticMd },
|
|
53
|
-
{ fileName: "explore.md", template: exploreMd },
|
|
54
|
-
{ fileName: "plan.md", template: planMd },
|
|
55
|
-
{ fileName: "reviewer.md", template: reviewerMd },
|
|
56
|
-
{
|
|
57
|
-
fileName: "task.md",
|
|
58
|
-
frontmatter: {
|
|
59
|
-
name: "task",
|
|
60
|
-
description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
|
|
61
|
-
spawns: "*",
|
|
62
|
-
model: "pi/default",
|
|
63
|
-
thinkingLevel: Effort.Medium,
|
|
64
|
-
hide: true,
|
|
65
|
-
},
|
|
66
|
-
template: taskMd,
|
|
67
|
-
},
|
|
68
47
|
];
|
|
69
48
|
|
|
70
49
|
// Computed lazily on first loadBundledAgents() call to avoid eager prompt.render at module load.
|
package/src/task/index.ts
CHANGED
|
@@ -55,7 +55,7 @@ import { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoiSummary } from "./
|
|
|
55
55
|
import { renderResult, renderCall as renderTaskCall } from "./render";
|
|
56
56
|
import { reconcileSpawnRoi } from "./roi-reconciliation";
|
|
57
57
|
import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
|
|
58
|
-
import { DEFAULT_SPAWN_THRESHOLD,
|
|
58
|
+
import { DEFAULT_SPAWN_THRESHOLD, evaluateSpawnGate } from "./spawn-gate";
|
|
59
59
|
import {
|
|
60
60
|
applyNestedPatches,
|
|
61
61
|
captureBaseline,
|
|
@@ -359,7 +359,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
359
359
|
readonly renderResult = renderResult;
|
|
360
360
|
readonly #discoveredAgents: AgentDefinition[];
|
|
361
361
|
readonly #blockedAgent: string | undefined;
|
|
362
|
-
readonly #spawningAgentType: string | undefined;
|
|
363
362
|
|
|
364
363
|
get parameters(): TaskToolSchemaInstance {
|
|
365
364
|
const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
|
|
@@ -391,7 +390,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
391
390
|
discoveredAgents: AgentDefinition[],
|
|
392
391
|
) {
|
|
393
392
|
this.#blockedAgent = $env.PI_BLOCKED_AGENT;
|
|
394
|
-
this.#spawningAgentType = session.currentAgentType;
|
|
395
393
|
this.#discoveredAgents = discoveredAgents;
|
|
396
394
|
}
|
|
397
395
|
|
|
@@ -478,23 +476,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
478
476
|
};
|
|
479
477
|
}
|
|
480
478
|
|
|
481
|
-
const reviewerExploreDecision = evaluateReviewerExploreGate({
|
|
482
|
-
spawningAgentType: this.#spawningAgentType,
|
|
483
|
-
targetAgent: params.agent,
|
|
484
|
-
plan: params.spawnPlan,
|
|
485
|
-
});
|
|
486
|
-
if (reviewerExploreDecision.outcome === "rejected") {
|
|
487
|
-
return {
|
|
488
|
-
content: [
|
|
489
|
-
{
|
|
490
|
-
type: "text",
|
|
491
|
-
text: `Task spawn gate rejected reviewer->explore: ${reviewerExploreDecision.reason}. Provide spawnPlan fields: ${reviewerExploreDecision.missingFields.join(", ")}.`,
|
|
492
|
-
},
|
|
493
|
-
],
|
|
494
|
-
details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
|
|
498
479
|
const manager = AsyncJobManager.instance();
|
|
499
480
|
if (!manager) {
|
|
500
481
|
return {
|
|
@@ -1083,27 +1064,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1083
1064
|
};
|
|
1084
1065
|
}
|
|
1085
1066
|
|
|
1086
|
-
const reviewerExploreDecision = evaluateReviewerExploreGate({
|
|
1087
|
-
spawningAgentType: this.#spawningAgentType,
|
|
1088
|
-
targetAgent: agentName,
|
|
1089
|
-
plan: params.spawnPlan,
|
|
1090
|
-
});
|
|
1091
|
-
if (reviewerExploreDecision.outcome === "rejected") {
|
|
1092
|
-
return {
|
|
1093
|
-
content: [
|
|
1094
|
-
{
|
|
1095
|
-
type: "text",
|
|
1096
|
-
text: `Task spawn gate rejected reviewer->explore: ${reviewerExploreDecision.reason}. Provide spawnPlan fields: ${reviewerExploreDecision.missingFields.join(", ")}.`,
|
|
1097
|
-
},
|
|
1098
|
-
],
|
|
1099
|
-
details: {
|
|
1100
|
-
projectAgentsDir,
|
|
1101
|
-
results: [],
|
|
1102
|
-
totalDurationMs: Date.now() - startTime,
|
|
1103
|
-
},
|
|
1104
|
-
};
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
1067
|
let repoRoot: string | null = null;
|
|
1108
1068
|
let baseline: WorktreeBaseline | null = null;
|
|
1109
1069
|
if (isIsolated) {
|