@gajae-code/coding-agent 0.2.4 → 0.3.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 +27 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +145 -2
- package/dist/types/commands/harness.d.ts +37 -0
- package/dist/types/config/settings-schema.d.ts +13 -3
- package/dist/types/config/settings.d.ts +3 -1
- package/dist/types/deep-interview/render-middleware.d.ts +5 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +8 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +2 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
- package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
- package/dist/types/harness-control-plane/classifier.d.ts +13 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
- package/dist/types/harness-control-plane/finalize.d.ts +47 -0
- package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
- package/dist/types/harness-control-plane/operate.d.ts +35 -0
- package/dist/types/harness-control-plane/owner.d.ts +46 -0
- package/dist/types/harness-control-plane/preserve.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +88 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
- package/dist/types/harness-control-plane/seams.d.ts +21 -0
- package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
- package/dist/types/harness-control-plane/storage.d.ts +53 -0
- package/dist/types/harness-control-plane/types.d.ts +162 -0
- package/dist/types/hooks/skill-keywords.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +2 -29
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/theme/defaults/index.d.ts +45 -9477
- package/dist/types/modes/theme/theme.d.ts +1 -5
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/skill-state/active-state.d.ts +3 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/types.d.ts +56 -3
- package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +24 -0
- package/dist/types/tools/cron.d.ts +110 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/monitor.d.ts +54 -0
- package/dist/types/tools/subagent.d.ts +11 -1
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/provider.d.ts +11 -4
- package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +522 -6
- package/src/cli/agents-cli.ts +3 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/config-cli.ts +10 -2
- package/src/cli.ts +2 -0
- package/src/commands/harness.ts +592 -0
- package/src/commands/team.ts +36 -39
- package/src/config/settings-schema.ts +15 -2
- package/src/config/settings.ts +49 -7
- package/src/deep-interview/render-middleware.ts +366 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +9 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
- package/src/defaults/gjc/skills/team/SKILL.md +47 -21
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
- package/src/discovery/helpers.ts +5 -0
- package/src/eval/js/shared/rewrite-imports.ts +1 -2
- package/src/exec/bash-executor.ts +20 -9
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
- package/src/gjc-runtime/goal-mode-request.ts +11 -3
- package/src/gjc-runtime/ralplan-runtime.ts +27 -10
- package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
- package/src/gjc-runtime/state-graph.ts +86 -0
- package/src/gjc-runtime/state-migrations.ts +132 -0
- package/src/gjc-runtime/state-renderer.ts +345 -0
- package/src/gjc-runtime/state-runtime.ts +733 -21
- package/src/gjc-runtime/state-validation.ts +49 -0
- package/src/gjc-runtime/state-writer.ts +718 -0
- package/src/gjc-runtime/team-runtime.ts +1083 -89
- package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
- package/src/gjc-runtime/workflow-manifest.ts +425 -0
- package/src/harness-control-plane/classifier.ts +128 -0
- package/src/harness-control-plane/control-endpoint.ts +137 -0
- package/src/harness-control-plane/finalize.ts +222 -0
- package/src/harness-control-plane/frame-mapper.ts +286 -0
- package/src/harness-control-plane/operate.ts +225 -0
- package/src/harness-control-plane/owner.ts +553 -0
- package/src/harness-control-plane/preserve.ts +102 -0
- package/src/harness-control-plane/receipts.ts +216 -0
- package/src/harness-control-plane/rpc-adapter.ts +276 -0
- package/src/harness-control-plane/seams.ts +39 -0
- package/src/harness-control-plane/session-lease.ts +388 -0
- package/src/harness-control-plane/state-machine.ts +97 -0
- package/src/harness-control-plane/storage.ts +257 -0
- package/src/harness-control-plane/types.ts +214 -0
- package/src/hooks/skill-keywords.ts +4 -2
- package/src/hooks/skill-state.ts +25 -42
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/lsp/render.ts +1 -1
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/acp/acp-client-bridge.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +1 -1
- package/src/modes/components/assistant-message.ts +5 -1
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/hook-selector.ts +72 -2
- package/src/modes/components/skill-hud/render.ts +7 -2
- package/src/modes/controllers/event-controller.ts +71 -6
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +19 -3
- package/src/modes/controllers/selector-controller.ts +3 -2
- package/src/modes/interactive-mode.ts +21 -2
- package/src/modes/theme/defaults/index.ts +0 -196
- package/src/modes/theme/theme.ts +35 -35
- package/src/modes/types.ts +2 -0
- package/src/prompts/agents/architect.md +5 -1
- package/src/prompts/agents/critic.md +5 -1
- package/src/prompts/agents/executor.md +13 -0
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/agents/planner.md +5 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/cron.md +25 -0
- package/src/prompts/tools/monitor.md +30 -0
- package/src/prompts/tools/subagent.md +33 -3
- package/src/runtime-mcp/oauth-flow.ts +4 -2
- package/src/sdk.ts +7 -0
- package/src/session/agent-session.ts +247 -38
- package/src/session/session-manager.ts +13 -1
- package/src/session/streaming-output.ts +21 -0
- package/src/skill-state/active-state.ts +222 -78
- package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
- package/src/skill-state/initial-phase.ts +2 -0
- package/src/skill-state/workflow-state-contract.ts +26 -0
- package/src/task/agents.ts +1 -0
- package/src/task/executor.ts +51 -8
- package/src/task/index.ts +120 -8
- package/src/task/render.ts +6 -3
- package/src/task/types.ts +57 -3
- package/src/tools/ask.ts +28 -7
- package/src/tools/bash-allowed-prefixes.ts +169 -0
- package/src/tools/bash.ts +190 -29
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/cron.ts +665 -0
- package/src/tools/index.ts +20 -2
- package/src/tools/monitor.ts +136 -0
- package/src/tools/subagent.ts +255 -64
- package/src/vim/engine.ts +3 -3
- package/src/web/search/index.ts +31 -18
- package/src/web/search/provider.ts +57 -12
- package/src/web/search/providers/duckduckgo.ts +279 -0
- package/src/web/search/types.ts +2 -0
- package/src/modes/theme/dark.json +0 -95
- package/src/modes/theme/defaults/alabaster.json +0 -93
- package/src/modes/theme/defaults/amethyst.json +0 -96
- package/src/modes/theme/defaults/anthracite.json +0 -93
- package/src/modes/theme/defaults/basalt.json +0 -91
- package/src/modes/theme/defaults/birch.json +0 -95
- package/src/modes/theme/defaults/dark-abyss.json +0 -91
- package/src/modes/theme/defaults/dark-arctic.json +0 -104
- package/src/modes/theme/defaults/dark-aurora.json +0 -95
- package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
- package/src/modes/theme/defaults/dark-cavern.json +0 -91
- package/src/modes/theme/defaults/dark-copper.json +0 -95
- package/src/modes/theme/defaults/dark-cosmos.json +0 -90
- package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
- package/src/modes/theme/defaults/dark-dracula.json +0 -98
- package/src/modes/theme/defaults/dark-eclipse.json +0 -91
- package/src/modes/theme/defaults/dark-ember.json +0 -95
- package/src/modes/theme/defaults/dark-equinox.json +0 -90
- package/src/modes/theme/defaults/dark-forest.json +0 -96
- package/src/modes/theme/defaults/dark-github.json +0 -105
- package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
- package/src/modes/theme/defaults/dark-lavender.json +0 -95
- package/src/modes/theme/defaults/dark-lunar.json +0 -89
- package/src/modes/theme/defaults/dark-midnight.json +0 -95
- package/src/modes/theme/defaults/dark-monochrome.json +0 -94
- package/src/modes/theme/defaults/dark-monokai.json +0 -98
- package/src/modes/theme/defaults/dark-nebula.json +0 -90
- package/src/modes/theme/defaults/dark-nord.json +0 -97
- package/src/modes/theme/defaults/dark-ocean.json +0 -101
- package/src/modes/theme/defaults/dark-one.json +0 -100
- package/src/modes/theme/defaults/dark-poimandres.json +0 -141
- package/src/modes/theme/defaults/dark-rainforest.json +0 -91
- package/src/modes/theme/defaults/dark-reef.json +0 -91
- package/src/modes/theme/defaults/dark-retro.json +0 -92
- package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
- package/src/modes/theme/defaults/dark-sakura.json +0 -95
- package/src/modes/theme/defaults/dark-slate.json +0 -95
- package/src/modes/theme/defaults/dark-solarized.json +0 -97
- package/src/modes/theme/defaults/dark-solstice.json +0 -90
- package/src/modes/theme/defaults/dark-starfall.json +0 -91
- package/src/modes/theme/defaults/dark-sunset.json +0 -99
- package/src/modes/theme/defaults/dark-swamp.json +0 -90
- package/src/modes/theme/defaults/dark-synthwave.json +0 -103
- package/src/modes/theme/defaults/dark-taiga.json +0 -91
- package/src/modes/theme/defaults/dark-terminal.json +0 -95
- package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
- package/src/modes/theme/defaults/dark-tundra.json +0 -91
- package/src/modes/theme/defaults/dark-twilight.json +0 -91
- package/src/modes/theme/defaults/dark-volcanic.json +0 -91
- package/src/modes/theme/defaults/graphite.json +0 -92
- package/src/modes/theme/defaults/light-arctic.json +0 -107
- package/src/modes/theme/defaults/light-aurora-day.json +0 -91
- package/src/modes/theme/defaults/light-canyon.json +0 -91
- package/src/modes/theme/defaults/light-catppuccin.json +0 -106
- package/src/modes/theme/defaults/light-cirrus.json +0 -90
- package/src/modes/theme/defaults/light-coral.json +0 -95
- package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
- package/src/modes/theme/defaults/light-dawn.json +0 -90
- package/src/modes/theme/defaults/light-dunes.json +0 -91
- package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
- package/src/modes/theme/defaults/light-forest.json +0 -100
- package/src/modes/theme/defaults/light-frost.json +0 -95
- package/src/modes/theme/defaults/light-github.json +0 -115
- package/src/modes/theme/defaults/light-glacier.json +0 -91
- package/src/modes/theme/defaults/light-gruvbox.json +0 -108
- package/src/modes/theme/defaults/light-haze.json +0 -90
- package/src/modes/theme/defaults/light-honeycomb.json +0 -95
- package/src/modes/theme/defaults/light-lagoon.json +0 -91
- package/src/modes/theme/defaults/light-lavender.json +0 -95
- package/src/modes/theme/defaults/light-meadow.json +0 -91
- package/src/modes/theme/defaults/light-mint.json +0 -95
- package/src/modes/theme/defaults/light-monochrome.json +0 -101
- package/src/modes/theme/defaults/light-ocean.json +0 -99
- package/src/modes/theme/defaults/light-one.json +0 -99
- package/src/modes/theme/defaults/light-opal.json +0 -91
- package/src/modes/theme/defaults/light-orchard.json +0 -91
- package/src/modes/theme/defaults/light-paper.json +0 -95
- package/src/modes/theme/defaults/light-poimandres.json +0 -141
- package/src/modes/theme/defaults/light-prism.json +0 -90
- package/src/modes/theme/defaults/light-retro.json +0 -98
- package/src/modes/theme/defaults/light-sand.json +0 -95
- package/src/modes/theme/defaults/light-savanna.json +0 -91
- package/src/modes/theme/defaults/light-solarized.json +0 -102
- package/src/modes/theme/defaults/light-soleil.json +0 -90
- package/src/modes/theme/defaults/light-sunset.json +0 -99
- package/src/modes/theme/defaults/light-synthwave.json +0 -98
- package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
- package/src/modes/theme/defaults/light-wetland.json +0 -91
- package/src/modes/theme/defaults/light-zenith.json +0 -89
- package/src/modes/theme/defaults/limestone.json +0 -94
- package/src/modes/theme/defaults/mahogany.json +0 -97
- package/src/modes/theme/defaults/marble.json +0 -93
- package/src/modes/theme/defaults/obsidian.json +0 -91
- package/src/modes/theme/defaults/onyx.json +0 -91
- package/src/modes/theme/defaults/pearl.json +0 -93
- package/src/modes/theme/defaults/porcelain.json +0 -91
- package/src/modes/theme/defaults/quartz.json +0 -96
- package/src/modes/theme/defaults/sandstone.json +0 -95
- package/src/modes/theme/defaults/titanium.json +0 -90
- package/src/modes/theme/light.json +0 -93
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
1
|
import * as path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
type ActiveSessionScope,
|
|
4
|
+
rebuildActiveSnapshot,
|
|
5
|
+
removeActiveEntry,
|
|
6
|
+
writeActiveEntry,
|
|
7
|
+
} from "../gjc-runtime/state-writer";
|
|
3
8
|
import type { WorkflowStateReceipt } from "./workflow-state-contract";
|
|
4
9
|
|
|
5
10
|
export const SKILL_ACTIVE_STATE_FILE = "skill-active-state.json";
|
|
@@ -56,6 +61,8 @@ export interface SkillActiveState {
|
|
|
56
61
|
session_id?: string;
|
|
57
62
|
thread_id?: string;
|
|
58
63
|
turn_id?: string;
|
|
64
|
+
initialized_mode?: CanonicalGjcWorkflowSkill;
|
|
65
|
+
initialized_state_path?: string;
|
|
59
66
|
active_skills?: SkillActiveEntry[];
|
|
60
67
|
[key: string]: unknown;
|
|
61
68
|
}
|
|
@@ -286,14 +293,6 @@ export function getSkillActiveStatePaths(cwd: string, sessionId?: string): Skill
|
|
|
286
293
|
};
|
|
287
294
|
}
|
|
288
295
|
|
|
289
|
-
async function readStateFile(filePath: string): Promise<SkillActiveState | null> {
|
|
290
|
-
try {
|
|
291
|
-
return normalizeSkillActiveState(JSON.parse(await Bun.file(filePath).text()));
|
|
292
|
-
} catch {
|
|
293
|
-
return null;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
296
|
/**
|
|
298
297
|
* Raw read for handoff mutations. Returns the *unnormalized* parsed object so
|
|
299
298
|
* inactive entries remain visible to `rawActiveEntries` — `normalizeSkillActiveState`
|
|
@@ -327,11 +326,33 @@ async function readRawActiveStateForHandoff(filePath: string, strict: boolean):
|
|
|
327
326
|
}
|
|
328
327
|
|
|
329
328
|
function rawActiveEntries(state: SkillActiveState | null): SkillActiveEntry[] {
|
|
330
|
-
if (!state
|
|
329
|
+
if (!state) return [];
|
|
331
330
|
const out: SkillActiveEntry[] = [];
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
331
|
+
if (Array.isArray(state.active_skills)) {
|
|
332
|
+
for (const candidate of state.active_skills) {
|
|
333
|
+
const normalized = normalizeEntry(candidate);
|
|
334
|
+
if (normalized) out.push(normalized);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Legacy top-level fallback: pre-`active_skills` state files persisted a single
|
|
338
|
+
// active workflow as top-level `{ active: true, skill, phase, … }` with no
|
|
339
|
+
// `active_skills` array. `normalizeSkillActiveState` still synthesizes that row,
|
|
340
|
+
// so the raw read used by the HUD, mutation guard, and caller inference must do
|
|
341
|
+
// the same or it would treat a legacy active workflow as absent.
|
|
342
|
+
if (out.length === 0 && state.active === true) {
|
|
343
|
+
const skill = safeString(state.skill).trim();
|
|
344
|
+
if (skill) {
|
|
345
|
+
out.push({
|
|
346
|
+
skill,
|
|
347
|
+
phase: safeString(state.phase).trim() || undefined,
|
|
348
|
+
active: true,
|
|
349
|
+
activated_at: safeString(state.activated_at).trim() || undefined,
|
|
350
|
+
updated_at: safeString(state.updated_at).trim() || undefined,
|
|
351
|
+
session_id: safeString(state.session_id).trim() || undefined,
|
|
352
|
+
thread_id: safeString(state.thread_id).trim() || undefined,
|
|
353
|
+
turn_id: safeString(state.turn_id).trim() || undefined,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
335
356
|
}
|
|
336
357
|
return out;
|
|
337
358
|
}
|
|
@@ -345,24 +366,139 @@ function filterRootEntriesForSession(entries: SkillActiveEntry[], sessionId?: st
|
|
|
345
366
|
});
|
|
346
367
|
}
|
|
347
368
|
|
|
369
|
+
function entryRecency(entry: SkillActiveEntry): number {
|
|
370
|
+
const stamp = entry.handoff_at || entry.updated_at || entry.activated_at;
|
|
371
|
+
const ms = stamp ? Date.parse(stamp) : Number.NaN;
|
|
372
|
+
// NaN signals "no trustworthy timestamp" so comparisons can refuse to let an
|
|
373
|
+
// unknown-recency row win a tie; callers must treat NaN explicitly.
|
|
374
|
+
return ms;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Session ownership rank for a row visible to a `sessionId` read. When a concrete
|
|
379
|
+
* session is in scope, a row owned by that exact session outranks a session-less
|
|
380
|
+
* fallback row, which outranks a foreign-session row. Session-less rows are global
|
|
381
|
+
* fallbacks and must never override a session's own state. With no scope session,
|
|
382
|
+
* every row ranks equally.
|
|
383
|
+
*/
|
|
384
|
+
function sessionScopeRank(entry: SkillActiveEntry, sessionId?: string): number {
|
|
385
|
+
const scope = safeString(sessionId).trim();
|
|
386
|
+
if (!scope) return 0;
|
|
387
|
+
const entrySession = safeString(entry.session_id).trim();
|
|
388
|
+
if (entrySession === scope) return 2;
|
|
389
|
+
if (entrySession.length === 0) return 1;
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Pick the surviving row for a single skill within a session-scoped visible set.
|
|
395
|
+
* Precedence, highest first:
|
|
396
|
+
* 1. exact-session ownership over a session-less fallback row,
|
|
397
|
+
* 2. a strictly-newer valid timestamp,
|
|
398
|
+
* 3. a valid timestamp over a missing/unparseable one,
|
|
399
|
+
* 4. active over inactive — so an untrustworthy inactive row can never hide an
|
|
400
|
+
* active row — then merge order for a total tie.
|
|
401
|
+
* A genuine handoff demotion still supersedes a stale active row of the same skill
|
|
402
|
+
* because, within one session scope, it carries the newest valid timestamp.
|
|
403
|
+
*/
|
|
404
|
+
function moreVisibleEntry(
|
|
405
|
+
incumbent: SkillActiveEntry,
|
|
406
|
+
challenger: SkillActiveEntry,
|
|
407
|
+
sessionId?: string,
|
|
408
|
+
): SkillActiveEntry {
|
|
409
|
+
const scopeDelta = sessionScopeRank(incumbent, sessionId) - sessionScopeRank(challenger, sessionId);
|
|
410
|
+
if (scopeDelta !== 0) return scopeDelta > 0 ? incumbent : challenger;
|
|
411
|
+
const ri = entryRecency(incumbent);
|
|
412
|
+
const rc = entryRecency(challenger);
|
|
413
|
+
const vi = Number.isFinite(ri);
|
|
414
|
+
const vc = Number.isFinite(rc);
|
|
415
|
+
if (vi && vc && ri !== rc) return ri > rc ? incumbent : challenger;
|
|
416
|
+
if (vi !== vc) return vi ? incumbent : challenger;
|
|
417
|
+
const incumbentActive = incumbent.active !== false;
|
|
418
|
+
const challengerActive = challenger.active !== false;
|
|
419
|
+
if (incumbentActive !== challengerActive) return incumbentActive ? incumbent : challenger;
|
|
420
|
+
return incumbent;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Collapse the merged, session-scoped entries down to a single row per skill.
|
|
425
|
+
* A handed-off skill can leave more than one row visible to a session — e.g. a
|
|
426
|
+
* row seeded without a session id (rendered globally by
|
|
427
|
+
* `filterRootEntriesForSession`) plus a later, session-scoped handoff demotion
|
|
428
|
+
* of the same skill. Without this collapse the HUD renders the same workflow
|
|
429
|
+
* twice and keeps showing a skill that has already handed control to its
|
|
430
|
+
* successor. `moreVisibleEntry` picks the winner so a handoff demotion supersedes
|
|
431
|
+
* an older stale `active:true` row (and is then dropped by the active filter
|
|
432
|
+
* below) while a session's own active row is never hidden by a session-less or
|
|
433
|
+
* untrustworthy-timestamp row.
|
|
434
|
+
*/
|
|
435
|
+
function dedupeVisibleBySkill(entries: SkillActiveEntry[], sessionId?: string): SkillActiveEntry[] {
|
|
436
|
+
const winners = new Map<string, SkillActiveEntry>();
|
|
437
|
+
for (const entry of entries) {
|
|
438
|
+
const current = winners.get(entry.skill);
|
|
439
|
+
winners.set(entry.skill, current ? moreVisibleEntry(current, entry, sessionId) : entry);
|
|
440
|
+
}
|
|
441
|
+
return [...winners.values()];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The planning pipeline advances one stage at a time: `deep-interview →
|
|
446
|
+
* ralplan → ultragoal`. Each stage is activated through its own command path
|
|
447
|
+
* (`gjc deep-interview`, `gjc ralplan`, `gjc ultragoal`), and those activations
|
|
448
|
+
* do not demote the previous stage's row — only the explicit `handoff` verb
|
|
449
|
+
* does. Without this collapse, activating ultragoal while ralplan is still
|
|
450
|
+
* `active:true` would render both stages and keep showing a workflow that has
|
|
451
|
+
* already handed control forward. Keep only the most recently updated pipeline
|
|
452
|
+
* stage so the HUD reflects the single current workflow. `team` is intentionally
|
|
453
|
+
* excluded — it runs alongside ultragoal — and every non-pipeline skill is left
|
|
454
|
+
* untouched.
|
|
455
|
+
*
|
|
456
|
+
* This is a HUD-display policy only. It is applied by the skill HUD renderer and
|
|
457
|
+
* deliberately NOT folded into `readVisibleSkillActiveState`, whose callers (the
|
|
458
|
+
* deep-interview mutation guard and handoff caller inference) must keep seeing
|
|
459
|
+
* every genuinely-active skill rather than the single most-recent pipeline stage.
|
|
460
|
+
*/
|
|
461
|
+
const PLANNING_PIPELINE_SKILLS = new Set<string>(["deep-interview", "ralplan", "ultragoal"]);
|
|
462
|
+
|
|
463
|
+
export function collapsePlanningPipeline(entries: readonly SkillActiveEntry[]): SkillActiveEntry[] {
|
|
464
|
+
const pipeline = entries.filter(entry => PLANNING_PIPELINE_SKILLS.has(entry.skill));
|
|
465
|
+
if (pipeline.length <= 1) return [...entries];
|
|
466
|
+
let current = pipeline[0];
|
|
467
|
+
let currentRecency = entryRecency(current);
|
|
468
|
+
for (const entry of pipeline) {
|
|
469
|
+
const recency = entryRecency(entry);
|
|
470
|
+
// Prefer a strictly-newer valid timestamp; a valid timestamp also beats a
|
|
471
|
+
// missing/unparseable one. Ties (or all-invalid) keep the first stage
|
|
472
|
+
// deterministically rather than letting an unknown-recency row win.
|
|
473
|
+
const better = Number.isFinite(recency) && (!Number.isFinite(currentRecency) || recency > currentRecency);
|
|
474
|
+
if (better) {
|
|
475
|
+
current = entry;
|
|
476
|
+
currentRecency = recency;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return entries.filter(entry => !PLANNING_PIPELINE_SKILLS.has(entry.skill) || entry === current);
|
|
480
|
+
}
|
|
481
|
+
|
|
348
482
|
function mergeVisibleEntries(
|
|
349
483
|
sessionState: SkillActiveState | null,
|
|
350
484
|
rootState: SkillActiveState | null,
|
|
351
485
|
sessionId?: string,
|
|
352
486
|
): SkillActiveEntry[] {
|
|
353
|
-
|
|
487
|
+
// Use the raw (active + inactive) rows so a handoff demotion stays visible
|
|
488
|
+
// long enough to supersede a stale same-skill row before the active filter.
|
|
489
|
+
const rootEntries = filterRootEntriesForSession(rawActiveEntries(rootState), sessionId);
|
|
354
490
|
const merged = new Map(rootEntries.map(entry => [entryKey(entry), entry]));
|
|
355
|
-
for (const entry of
|
|
491
|
+
for (const entry of rawActiveEntries(sessionState)) {
|
|
356
492
|
merged.set(entryKey(entry), entry);
|
|
357
493
|
}
|
|
358
|
-
return [...merged.values()];
|
|
494
|
+
return dedupeVisibleBySkill([...merged.values()], sessionId).filter(entry => entry.active !== false);
|
|
359
495
|
}
|
|
360
496
|
|
|
361
497
|
export async function readVisibleSkillActiveState(cwd: string, sessionId?: string): Promise<SkillActiveState | null> {
|
|
362
498
|
const { rootPath, sessionPath } = getSkillActiveStatePaths(cwd, sessionId);
|
|
363
499
|
const [rootState, sessionState] = await Promise.all([
|
|
364
|
-
|
|
365
|
-
sessionPath ?
|
|
500
|
+
readRawActiveStateForHandoff(rootPath, false),
|
|
501
|
+
sessionPath ? readRawActiveStateForHandoff(sessionPath, false) : Promise.resolve(null),
|
|
366
502
|
]);
|
|
367
503
|
const activeSkills = mergeVisibleEntries(sessionState, rootState, sessionId);
|
|
368
504
|
if (activeSkills.length === 0) return null;
|
|
@@ -379,15 +515,41 @@ export async function readVisibleSkillActiveState(cwd: string, sessionId?: strin
|
|
|
379
515
|
};
|
|
380
516
|
}
|
|
381
517
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
518
|
+
function activeStateWriterAudit(verb: string) {
|
|
519
|
+
return { category: "state" as const, verb, owner: "gjc-runtime" as const };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function persistActiveEntry(
|
|
523
|
+
cwd: string,
|
|
524
|
+
sessionScope: ActiveSessionScope | undefined,
|
|
525
|
+
entry: SkillActiveEntry,
|
|
526
|
+
): Promise<void> {
|
|
527
|
+
if (entry.active === false) {
|
|
528
|
+
await removeActiveEntry(cwd, sessionScope, entry.skill, {
|
|
529
|
+
cwd,
|
|
530
|
+
audit: activeStateWriterAudit("remove-active-entry"),
|
|
531
|
+
});
|
|
532
|
+
} else {
|
|
533
|
+
await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
|
|
534
|
+
cwd,
|
|
535
|
+
audit: activeStateWriterAudit("write-active-entry"),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function writeHandoffEntry(
|
|
541
|
+
cwd: string,
|
|
542
|
+
sessionScope: ActiveSessionScope | undefined,
|
|
543
|
+
entry: SkillActiveEntry,
|
|
544
|
+
): Promise<void> {
|
|
545
|
+
await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
|
|
546
|
+
cwd,
|
|
547
|
+
audit: activeStateWriterAudit("write-active-entry"),
|
|
548
|
+
});
|
|
385
549
|
}
|
|
386
550
|
|
|
387
|
-
function
|
|
388
|
-
|
|
389
|
-
const retained = entries.filter(candidate => entryKey(candidate) !== key);
|
|
390
|
-
return active ? [...retained, entry] : retained;
|
|
551
|
+
async function rebuildActiveState(cwd: string, sessionScope?: ActiveSessionScope): Promise<void> {
|
|
552
|
+
await rebuildActiveSnapshot(cwd, sessionScope, { cwd, audit: activeStateWriterAudit("rebuild-active-snapshot") });
|
|
391
553
|
}
|
|
392
554
|
|
|
393
555
|
export async function syncSkillActiveState(options: SyncSkillActiveStateOptions): Promise<void> {
|
|
@@ -408,36 +570,13 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
|
|
|
408
570
|
...(hud ? { hud } : {}),
|
|
409
571
|
...(options.receipt ? { receipt: options.receipt } : {}),
|
|
410
572
|
};
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const rootEntries = upsertEntry(listActiveSkills(rootState), entry, options.active);
|
|
414
|
-
const nextRoot: SkillActiveState = {
|
|
415
|
-
...rootState,
|
|
416
|
-
version: 1,
|
|
417
|
-
active: rootEntries.length > 0,
|
|
418
|
-
skill: rootEntries[0]?.skill ?? "",
|
|
419
|
-
phase: rootEntries[0]?.phase ?? "",
|
|
420
|
-
updated_at: nowIso,
|
|
421
|
-
source: options.source,
|
|
422
|
-
active_skills: rootEntries,
|
|
423
|
-
};
|
|
424
|
-
await writeStateFile(rootPath, nextRoot);
|
|
573
|
+
await persistActiveEntry(options.cwd, undefined, entry);
|
|
574
|
+
await rebuildActiveState(options.cwd);
|
|
425
575
|
|
|
426
|
-
if (!
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
...sessionState,
|
|
431
|
-
version: 1,
|
|
432
|
-
active: sessionEntries.length > 0,
|
|
433
|
-
skill: sessionEntries[0]?.skill ?? "",
|
|
434
|
-
phase: sessionEntries[0]?.phase ?? "",
|
|
435
|
-
session_id: options.sessionId,
|
|
436
|
-
updated_at: nowIso,
|
|
437
|
-
source: options.source,
|
|
438
|
-
active_skills: sessionEntries,
|
|
439
|
-
};
|
|
440
|
-
await writeStateFile(sessionPath, nextSession);
|
|
576
|
+
if (!options.sessionId) return;
|
|
577
|
+
const sessionScope = { sessionId: options.sessionId };
|
|
578
|
+
await persistActiveEntry(options.cwd, sessionScope, entry);
|
|
579
|
+
await rebuildActiveState(options.cwd, sessionScope);
|
|
441
580
|
}
|
|
442
581
|
|
|
443
582
|
export interface ApplyHandoffOptions {
|
|
@@ -467,12 +606,27 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
|
|
|
467
606
|
const sessionId = options.callee.sessionId ?? options.caller.sessionId;
|
|
468
607
|
const { rootPath, sessionPath } = getSkillActiveStatePaths(options.cwd, sessionId);
|
|
469
608
|
const readState = (filePath: string) => readRawActiveStateForHandoff(filePath, options.strict === true);
|
|
470
|
-
|
|
609
|
+
await Promise.all([readState(rootPath), ...(sessionPath ? [readState(sessionPath)] : [])]);
|
|
610
|
+
|
|
611
|
+
// A skill can hold more than one visible row in this session's scope — e.g.
|
|
612
|
+
// it was seeded without a session id (rendered globally) and is now handed
|
|
613
|
+
// off under a concrete session id. Supersede every same-session-scope row of
|
|
614
|
+
// the caller and callee skills, not just the exact `skill::session_id` key,
|
|
615
|
+
// so a stale `active:true` row cannot survive the demotion and keep showing
|
|
616
|
+
// in the HUD. Rows owned by other sessions are left untouched.
|
|
617
|
+
const handoffSession = safeString(sessionId).trim();
|
|
618
|
+
const reassignedSkills = new Set([callerEntry.skill, calleeEntry.skill]);
|
|
619
|
+
const supersedesVisible = (entry: SkillActiveEntry): boolean => {
|
|
620
|
+
if (!reassignedSkills.has(entry.skill)) return false;
|
|
621
|
+
const entrySession = safeString(entry.session_id).trim();
|
|
622
|
+
return entrySession.length === 0 || entrySession === handoffSession;
|
|
623
|
+
};
|
|
471
624
|
const applyEntries = (entries: SkillActiveEntry[]): SkillActiveEntry[] => {
|
|
472
625
|
const callerKey = entryKey(callerEntry);
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
626
|
+
const priorCaller =
|
|
627
|
+
entries.find(e => entryKey(e) === callerKey) ??
|
|
628
|
+
entries.find(e => e.skill === callerEntry.skill && supersedesVisible(e) && Boolean(e.handoff_from));
|
|
629
|
+
const kept = entries.filter(e => !supersedesVisible(e));
|
|
476
630
|
// Merge prior lineage into the demoted caller so multi-step handoff
|
|
477
631
|
// chains preserve `handoff_from` from the previous transition while
|
|
478
632
|
// the new `handoff_to`/`handoff_at` describe this one.
|
|
@@ -486,33 +640,23 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
|
|
|
486
640
|
: callerEntry;
|
|
487
641
|
return [...kept, mergedCaller, calleeEntry];
|
|
488
642
|
};
|
|
489
|
-
const
|
|
643
|
+
const writeEntries = async (
|
|
644
|
+
sessionScope: ActiveSessionScope | undefined,
|
|
490
645
|
prior: SkillActiveState | null,
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
version: 1,
|
|
498
|
-
active: visible.length > 0,
|
|
499
|
-
skill: visible[0]?.skill ?? "",
|
|
500
|
-
phase: visible[0]?.phase ?? "",
|
|
501
|
-
...(scope === "session" ? { session_id: sessionId } : {}),
|
|
502
|
-
updated_at: nowIso,
|
|
503
|
-
source: options.callee.source ?? options.caller.source,
|
|
504
|
-
active_skills: entries,
|
|
505
|
-
};
|
|
646
|
+
): Promise<void> => {
|
|
647
|
+
const nextEntries = applyEntries(rawActiveEntries(prior));
|
|
648
|
+
for (const entry of nextEntries) {
|
|
649
|
+
await writeHandoffEntry(options.cwd, sessionScope, entry);
|
|
650
|
+
}
|
|
651
|
+
await rebuildActiveState(options.cwd, sessionScope);
|
|
506
652
|
};
|
|
507
653
|
|
|
508
654
|
if (sessionPath) {
|
|
509
655
|
const prior = await readState(sessionPath);
|
|
510
|
-
|
|
511
|
-
await writeStateFile(sessionPath, next);
|
|
656
|
+
await writeEntries({ sessionId }, prior);
|
|
512
657
|
}
|
|
513
658
|
const priorRoot = await readState(rootPath);
|
|
514
|
-
|
|
515
|
-
await writeStateFile(rootPath, nextRoot);
|
|
659
|
+
await writeEntries(undefined, priorRoot);
|
|
516
660
|
}
|
|
517
661
|
|
|
518
662
|
function buildSyncEntry(options: SyncSkillActiveStateOptions, nowIso: string): SkillActiveEntry {
|
|
@@ -14,12 +14,16 @@ import {
|
|
|
14
14
|
export const DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE =
|
|
15
15
|
"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 `gjc deep-interview --write --stage final` or hand off to an execution phase.";
|
|
16
16
|
export const WORKFLOW_STATE_MUTATION_BLOCK_MESSAGE =
|
|
17
|
-
"
|
|
17
|
+
".gjc workflow state and artifacts are runtime-owned. Agent mutation tools cannot edit `.gjc/**`; use the sanctioned `gjc` CLI instead.";
|
|
18
18
|
|
|
19
|
-
const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit"]);
|
|
19
|
+
const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit", "bash"]);
|
|
20
20
|
const ARCHIVE_OR_SQLITE_BASE_RE = /^(.+?\.(?:tar\.gz|sqlite3|sqlite|db3|zip|tgz|tar|db))(?:$|:)/i;
|
|
21
21
|
const INTERNAL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
22
22
|
const VIM_FILE_SWITCH_RE = /^\s*:(?:e|e!|edit|edit!)(?:\s+([^<\r\n]+))?(?:<CR>|\r|\n|$)/i;
|
|
23
|
+
const BASH_TOKEN_RE = /'[^']*'|"(?:\\.|[^"\\])*"|\S+/g;
|
|
24
|
+
const BASH_REDIRECT_RE = /^(?:\d*)>>?$/;
|
|
25
|
+
const BASH_HEREDOC_RE = /^(?:\d*)<<-?$/;
|
|
26
|
+
const BASH_MUTATION_COMMANDS = new Set(["rm", "mv", "cp", "touch", "mkdir", "ln", "tee"]);
|
|
23
27
|
|
|
24
28
|
type ToolWithEditMode = AgentTool & {
|
|
25
29
|
mode?: unknown;
|
|
@@ -219,10 +223,75 @@ function extractEditTargets(args: unknown, tool: ToolWithEditMode): ExtractedTar
|
|
|
219
223
|
return targets;
|
|
220
224
|
}
|
|
221
225
|
|
|
226
|
+
function extractBashTargets(args: unknown): ExtractedTargets {
|
|
227
|
+
const record = getRecord(args);
|
|
228
|
+
const command = safeString(record?.command).trim();
|
|
229
|
+
const targets: ExtractedTargets = { paths: [], unknown: false };
|
|
230
|
+
if (!command) {
|
|
231
|
+
targets.unknown = true;
|
|
232
|
+
return targets;
|
|
233
|
+
}
|
|
234
|
+
if (/^gjc(?:\s|$)/.test(command)) return targets;
|
|
235
|
+
|
|
236
|
+
const tokens = command.match(BASH_TOKEN_RE)?.map(unquoteBashToken) ?? [];
|
|
237
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
238
|
+
const token = tokens[index] ?? "";
|
|
239
|
+
if (BASH_REDIRECT_RE.test(token)) {
|
|
240
|
+
addPath(targets, tokens[index + 1]);
|
|
241
|
+
index++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const redirectMatch = token.match(/^(?:\d*)>>?(.+)$/);
|
|
245
|
+
if (redirectMatch?.[1]) {
|
|
246
|
+
addPath(targets, redirectMatch[1]);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (BASH_HEREDOC_RE.test(token)) {
|
|
250
|
+
addPath(targets, tokens[index + 1]);
|
|
251
|
+
index++;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const heredocMatch = token.match(/^(?:\d*)<<-?(.+)$/);
|
|
255
|
+
if (heredocMatch?.[1]) {
|
|
256
|
+
addPath(targets, heredocMatch[1]);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (isMutationBashCommand(tokens, index)) {
|
|
260
|
+
for (let targetIndex = index + 1; targetIndex < tokens.length; targetIndex++) {
|
|
261
|
+
const target = tokens[targetIndex] ?? "";
|
|
262
|
+
if (isBashCommandBoundary(target)) break;
|
|
263
|
+
if (target.startsWith("-")) continue;
|
|
264
|
+
addPath(targets, target);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return targets;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function unquoteBashToken(token: string): string {
|
|
272
|
+
if (token.length < 2) return token;
|
|
273
|
+
const quote = token[0];
|
|
274
|
+
if ((quote === "'" || quote === '"') && token.at(-1) === quote) return token.slice(1, -1);
|
|
275
|
+
return token;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isBashCommandBoundary(token: string): boolean {
|
|
279
|
+
return [";", "&&", "||", "|"].includes(token);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isMutationBashCommand(tokens: string[], index: number): boolean {
|
|
283
|
+
const token = path.basename(tokens[index] ?? "");
|
|
284
|
+
if (BASH_MUTATION_COMMANDS.has(token)) return true;
|
|
285
|
+
if (token !== "sed") return false;
|
|
286
|
+
const next = tokens[index + 1] ?? "";
|
|
287
|
+
return next === "-i" || next.startsWith("-i") || next.includes("i");
|
|
288
|
+
}
|
|
289
|
+
|
|
222
290
|
function extractTargets(tool: ToolWithEditMode, args: unknown): ExtractedTargets {
|
|
223
291
|
if (tool.name === "write") return extractWriteTargets(args);
|
|
224
292
|
if (tool.name === "ast_edit") return extractAstEditTargets(args);
|
|
225
293
|
if (tool.name === "edit") return extractEditTargets(args, tool);
|
|
294
|
+
if (tool.name === "bash") return extractBashTargets(args);
|
|
226
295
|
return { paths: [], unknown: true };
|
|
227
296
|
}
|
|
228
297
|
|
|
@@ -289,6 +358,14 @@ function isAllowlistedPath(cwd: string, rawPath: string): boolean {
|
|
|
289
358
|
if (segments?.[0] !== ".gjc") return false;
|
|
290
359
|
return segments[1] === "specs" || segments[1] === "plans";
|
|
291
360
|
}
|
|
361
|
+
function isBlockedGjcPath(cwd: string, rawPath: string): boolean {
|
|
362
|
+
const segments = relativeGjcSegments(cwd, rawPath);
|
|
363
|
+
return segments?.[0] === ".gjc";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function hasBlockedGjcTarget(cwd: string, targets: ExtractedTargets): boolean {
|
|
367
|
+
return targets.paths.some(rawPath => isBlockedGjcPath(cwd, rawPath));
|
|
368
|
+
}
|
|
292
369
|
|
|
293
370
|
function allTargetsAllowlisted(cwd: string, targets: ExtractedTargets): boolean {
|
|
294
371
|
return (
|
|
@@ -315,18 +392,16 @@ export async function getDeepInterviewMutationDecision(
|
|
|
315
392
|
): Promise<DeepInterviewMutationDecision> {
|
|
316
393
|
if (!BLOCKED_TOOL_NAMES.has(input.tool.name)) return { blocked: false, targets: [] };
|
|
317
394
|
const targets = extractTargets(input.tool, input.args);
|
|
318
|
-
if (input.enforceWorkflowState !== false) {
|
|
395
|
+
if (input.enforceWorkflowState !== false && hasBlockedGjcTarget(input.cwd, targets)) {
|
|
319
396
|
const stateSkill = firstBlockedWorkflowStateSkill(input.cwd, targets);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
};
|
|
329
|
-
}
|
|
397
|
+
const command = stateSkill ? sanctionedWorkflowStateCommand(stateSkill) : "gjc <workflow-command>";
|
|
398
|
+
return {
|
|
399
|
+
blocked: true,
|
|
400
|
+
message: `${WORKFLOW_STATE_MUTATION_BLOCK_MESSAGE}\nUse: ${command}`,
|
|
401
|
+
targets: targets.paths,
|
|
402
|
+
reason: stateSkill ? "workflow-state-target" : "gjc-target",
|
|
403
|
+
command,
|
|
404
|
+
};
|
|
330
405
|
}
|
|
331
406
|
if (!(await isActiveDeepInterview(input.cwd, input.sessionId, input.threadId))) {
|
|
332
407
|
return { blocked: false, targets: [] };
|
|
@@ -340,6 +415,9 @@ export async function getDeepInterviewMutationDecision(
|
|
|
340
415
|
reason: "unknown-target",
|
|
341
416
|
};
|
|
342
417
|
}
|
|
418
|
+
if (input.tool.name === "bash") {
|
|
419
|
+
return { blocked: false, targets: targets.paths };
|
|
420
|
+
}
|
|
343
421
|
return {
|
|
344
422
|
blocked: true,
|
|
345
423
|
message: DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE,
|
|
@@ -13,5 +13,7 @@ import type { CanonicalGjcWorkflowSkill } from "./active-state";
|
|
|
13
13
|
export function initialPhaseForSkill(skill: CanonicalGjcWorkflowSkill | string): string {
|
|
14
14
|
if (skill === "deep-interview") return "interviewing";
|
|
15
15
|
if (skill === "ultragoal") return "goal-planning";
|
|
16
|
+
if (skill === "ralplan") return "planner";
|
|
17
|
+
if (skill === "team") return "starting";
|
|
16
18
|
return "planning";
|
|
17
19
|
}
|
|
@@ -9,6 +9,13 @@ export const WORKFLOW_STATE_RECEIPT_FRESH_MS = 30 * 60 * 1000;
|
|
|
9
9
|
export type WorkflowStateMutationOwner = "gjc-state-cli" | "gjc-runtime" | "gjc-hook";
|
|
10
10
|
export type WorkflowStateReceiptStatus = "fresh" | "stale";
|
|
11
11
|
|
|
12
|
+
export interface WorkflowStateContentChecksum {
|
|
13
|
+
algorithm: "sha256";
|
|
14
|
+
value: string;
|
|
15
|
+
covered_path: string;
|
|
16
|
+
computed_at: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
export interface WorkflowStateReceipt {
|
|
13
20
|
version: 1;
|
|
14
21
|
skill: CanonicalGjcWorkflowSkill;
|
|
@@ -20,6 +27,25 @@ export interface WorkflowStateReceipt {
|
|
|
20
27
|
fresh_until: string;
|
|
21
28
|
status: WorkflowStateReceiptStatus;
|
|
22
29
|
mutation_id: string;
|
|
30
|
+
verb?: string;
|
|
31
|
+
from_phase?: string;
|
|
32
|
+
to_phase?: string;
|
|
33
|
+
forced?: boolean;
|
|
34
|
+
paths?: string[];
|
|
35
|
+
content_sha256?: WorkflowStateContentChecksum;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AuditEntry {
|
|
39
|
+
ts: string;
|
|
40
|
+
skill?: string;
|
|
41
|
+
category: string;
|
|
42
|
+
verb: string;
|
|
43
|
+
owner: WorkflowStateMutationOwner;
|
|
44
|
+
mutation_id: string;
|
|
45
|
+
from_phase?: string;
|
|
46
|
+
to_phase?: string;
|
|
47
|
+
forced: boolean;
|
|
48
|
+
paths: string[];
|
|
23
49
|
}
|
|
24
50
|
|
|
25
51
|
function safeString(value: unknown): string {
|