@gajae-code/coding-agent 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +29 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +7 -0
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +72 -2
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +39 -3
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +27 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +5 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli/setup-cli.ts +138 -3
- package/src/cli.ts +1 -0
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +7 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +193 -0
- package/src/config/file-lock.ts +66 -10
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +39 -30
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +459 -3
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +457 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +232 -19
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +48 -30
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +179 -2
- package/src/gjc-runtime/tmux-common.ts +14 -0
- package/src/gjc-runtime/tmux-gc.ts +177 -0
- package/src/gjc-runtime/tmux-sessions.ts +49 -1
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1239 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +14 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/harness-control-plane/storage.ts +70 -0
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/docs-index.generated.ts +22 -12
- package/src/lsp/defaults.json +1 -0
- package/src/main.ts +18 -3
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +2 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +51 -8
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +81 -1
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +266 -34
- package/src/modes/shared/agent-wire/command-dispatch.ts +281 -261
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +32 -2
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/prompts/agents/executor.md +5 -2
- package/src/sdk.ts +29 -4
- package/src/session/agent-session.ts +99 -19
- package/src/session/blob-store.ts +59 -3
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +72 -20
- package/src/setup/credential-import.ts +429 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +16 -1
- package/src/task/render.ts +18 -7
- package/src/tools/ask.ts +59 -2
- package/src/tools/cron.ts +1 -1
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/subagent-render.ts +128 -29
- package/src/tools/subagent.ts +173 -9
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
|
@@ -1523,11 +1523,51 @@
|
|
|
1523
1523
|
"name": "order-json",
|
|
1524
1524
|
"type": "string"
|
|
1525
1525
|
},
|
|
1526
|
+
{
|
|
1527
|
+
"appliesToVerbs": [
|
|
1528
|
+
"review"
|
|
1529
|
+
],
|
|
1530
|
+
"name": "pr",
|
|
1531
|
+
"type": "string"
|
|
1532
|
+
},
|
|
1533
|
+
{
|
|
1534
|
+
"appliesToVerbs": [
|
|
1535
|
+
"review"
|
|
1536
|
+
],
|
|
1537
|
+
"name": "branch",
|
|
1538
|
+
"type": "string"
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
"appliesToVerbs": [
|
|
1542
|
+
"review"
|
|
1543
|
+
],
|
|
1544
|
+
"name": "spec",
|
|
1545
|
+
"type": "string"
|
|
1546
|
+
},
|
|
1547
|
+
{
|
|
1548
|
+
"appliesToVerbs": [
|
|
1549
|
+
"review"
|
|
1550
|
+
],
|
|
1551
|
+
"name": "executor-qa-json",
|
|
1552
|
+
"type": "string"
|
|
1553
|
+
},
|
|
1554
|
+
{
|
|
1555
|
+
"appliesToVerbs": [
|
|
1556
|
+
"review"
|
|
1557
|
+
],
|
|
1558
|
+
"enumValues": [
|
|
1559
|
+
"review-only",
|
|
1560
|
+
"review-start"
|
|
1561
|
+
],
|
|
1562
|
+
"name": "mode",
|
|
1563
|
+
"type": "enum"
|
|
1564
|
+
},
|
|
1526
1565
|
{
|
|
1527
1566
|
"appliesToVerbs": [
|
|
1528
1567
|
"status",
|
|
1529
1568
|
"create-goals",
|
|
1530
1569
|
"complete-goals",
|
|
1570
|
+
"review",
|
|
1531
1571
|
"checkpoint",
|
|
1532
1572
|
"record-review-blockers",
|
|
1533
1573
|
"steer"
|
|
@@ -1599,6 +1639,10 @@
|
|
|
1599
1639
|
"name": "checkpoint",
|
|
1600
1640
|
"surface": "command-positional"
|
|
1601
1641
|
},
|
|
1642
|
+
{
|
|
1643
|
+
"name": "review",
|
|
1644
|
+
"surface": "command-positional"
|
|
1645
|
+
},
|
|
1602
1646
|
{
|
|
1603
1647
|
"name": "record-review-blockers",
|
|
1604
1648
|
"surface": "command-positional"
|
|
@@ -235,6 +235,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
|
|
|
235
235
|
"create-goals",
|
|
236
236
|
"complete-goals",
|
|
237
237
|
"checkpoint",
|
|
238
|
+
"review",
|
|
238
239
|
"record-review-blockers",
|
|
239
240
|
"steer",
|
|
240
241
|
]),
|
|
@@ -286,6 +287,16 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
|
|
|
286
287
|
{ name: "rationale", type: "string", appliesToVerbs: ["steer"] },
|
|
287
288
|
{ name: "replacements-json", type: "string", appliesToVerbs: ["steer"] },
|
|
288
289
|
{ name: "order-json", type: "string", appliesToVerbs: ["steer"] },
|
|
290
|
+
{ name: "pr", type: "string", appliesToVerbs: ["review"] },
|
|
291
|
+
{ name: "branch", type: "string", appliesToVerbs: ["review"] },
|
|
292
|
+
{ name: "spec", type: "string", appliesToVerbs: ["review"] },
|
|
293
|
+
{ name: "executor-qa-json", type: "string", appliesToVerbs: ["review"] },
|
|
294
|
+
{
|
|
295
|
+
name: "mode",
|
|
296
|
+
type: "enum",
|
|
297
|
+
enumValues: ["review-only", "review-start"],
|
|
298
|
+
appliesToVerbs: ["review"],
|
|
299
|
+
},
|
|
289
300
|
{
|
|
290
301
|
name: "json",
|
|
291
302
|
type: "boolean",
|
|
@@ -293,6 +304,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
|
|
|
293
304
|
"status",
|
|
294
305
|
"create-goals",
|
|
295
306
|
"complete-goals",
|
|
307
|
+
"review",
|
|
296
308
|
"checkpoint",
|
|
297
309
|
"record-review-blockers",
|
|
298
310
|
"steer",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type GcCollectResult,
|
|
5
|
+
type GcContext,
|
|
6
|
+
type GcError,
|
|
7
|
+
type GcPruneOutcome,
|
|
8
|
+
type GcRecord,
|
|
9
|
+
type GcStoreAdapter,
|
|
10
|
+
gcPidStatusLabel,
|
|
11
|
+
gcProbeToLeasePidStatus,
|
|
12
|
+
} from "../gjc-runtime/gc-runtime";
|
|
13
|
+
import { classifyLeaseStatus, readLease, reapDeadOwnerArtifacts } from "./session-lease";
|
|
14
|
+
import {
|
|
15
|
+
type HarnessRootRegistryForGc,
|
|
16
|
+
type HarnessRootRegistryListingForGc,
|
|
17
|
+
listHarnessRootRegistriesForGc,
|
|
18
|
+
removeHarnessRootRegistryFileForGc,
|
|
19
|
+
rewriteHarnessRootRegistryForGc,
|
|
20
|
+
sessionPaths,
|
|
21
|
+
} from "./storage";
|
|
22
|
+
|
|
23
|
+
function errorMessage(error: unknown): string {
|
|
24
|
+
return error instanceof Error ? error.message : String(error);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function exists(file: string): Promise<boolean> {
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(file);
|
|
30
|
+
return true;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function registryErrors(registries: HarnessRootRegistryListingForGc[]): GcError[] {
|
|
38
|
+
return registries
|
|
39
|
+
.filter(registry => registry.error)
|
|
40
|
+
.map(registry => ({
|
|
41
|
+
store: "registry_entries",
|
|
42
|
+
scope: registry.file,
|
|
43
|
+
message: registry.error ?? "registry_error",
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function collectRegistries(ctx: GcContext): Promise<HarnessRootRegistryListingForGc[]> {
|
|
48
|
+
return listHarnessRootRegistriesForGc(ctx.env);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const harnessLeasesGcAdapter: GcStoreAdapter = {
|
|
52
|
+
store: "harness_leases",
|
|
53
|
+
async collect(ctx: GcContext): Promise<GcCollectResult> {
|
|
54
|
+
const records: GcRecord[] = [];
|
|
55
|
+
const errors: GcError[] = [];
|
|
56
|
+
const registries = await collectRegistries(ctx);
|
|
57
|
+
errors.push(...registryErrors(registries).map(error => ({ ...error, store: "harness_leases" as const })));
|
|
58
|
+
|
|
59
|
+
const roots = new Set<string>();
|
|
60
|
+
for (const registry of registries) {
|
|
61
|
+
if (registry.error) continue;
|
|
62
|
+
for (const entry of registry.roots) roots.add(path.resolve(entry.root));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const root of roots) {
|
|
66
|
+
const sessionsDir = path.join(root, "sessions");
|
|
67
|
+
let sessionEntries: string[];
|
|
68
|
+
try {
|
|
69
|
+
sessionEntries = await fs.readdir(sessionsDir);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
72
|
+
if (code === "ENOENT") continue;
|
|
73
|
+
errors.push({ store: "harness_leases", scope: sessionsDir, message: errorMessage(error) });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const sessionId of sessionEntries) {
|
|
78
|
+
const sessionDir = sessionPaths(root, sessionId).dir;
|
|
79
|
+
try {
|
|
80
|
+
const stat = await fs.stat(sessionDir);
|
|
81
|
+
if (!stat.isDirectory()) continue;
|
|
82
|
+
const lease = await readLease(root, sessionId);
|
|
83
|
+
if (!lease) continue;
|
|
84
|
+
const status = classifyLeaseStatus(lease, { probe: gcProbeToLeasePidStatus(ctx.probe) });
|
|
85
|
+
const pidProbe = ctx.probe(lease.pid);
|
|
86
|
+
const pidStatus = gcPidStatusLabel(pidProbe);
|
|
87
|
+
const removable = status === "dead" && pidProbe.status === "dead";
|
|
88
|
+
records.push({
|
|
89
|
+
store: "harness_leases",
|
|
90
|
+
id: sessionId,
|
|
91
|
+
root,
|
|
92
|
+
path: sessionPaths(root, sessionId).lease,
|
|
93
|
+
pid: lease.pid,
|
|
94
|
+
pid_status: pidStatus,
|
|
95
|
+
status,
|
|
96
|
+
stale: status === "dead",
|
|
97
|
+
removable,
|
|
98
|
+
action: "none",
|
|
99
|
+
reason: removable
|
|
100
|
+
? `lease owner pid ${lease.pid} is dead`
|
|
101
|
+
: `lease owner pid ${lease.pid} is ${pidStatus}; keeping`,
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
errors.push({ store: "harness_leases", scope: sessionDir, message: errorMessage(error) });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { records, errors };
|
|
110
|
+
},
|
|
111
|
+
async prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome> {
|
|
112
|
+
if (!record.root) return { removed: false, skipped: "missing_root" };
|
|
113
|
+
const lease = await readLease(record.root, record.id);
|
|
114
|
+
if (!lease) return { removed: false, skipped: "lease_not_dead_or_missing" };
|
|
115
|
+
const status = classifyLeaseStatus(lease, { probe: gcProbeToLeasePidStatus(ctx.probe) });
|
|
116
|
+
if (status !== "dead") return { removed: false, skipped: "lease_not_dead_or_missing" };
|
|
117
|
+
const removed = await reapDeadOwnerArtifacts(record.root, record.id, lease.ownerId, lease.leaseEpoch, {
|
|
118
|
+
probe: gcProbeToLeasePidStatus(ctx.probe),
|
|
119
|
+
});
|
|
120
|
+
return removed ? { removed: true } : { removed: false, skipped: "reaper_guard_rejected" };
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
async function splitRegistryRoots(registry: HarnessRootRegistryForGc): Promise<{
|
|
125
|
+
liveRoots: HarnessRootRegistryForGc["roots"];
|
|
126
|
+
danglingRoots: HarnessRootRegistryForGc["roots"];
|
|
127
|
+
}> {
|
|
128
|
+
const liveRoots: HarnessRootRegistryForGc["roots"] = [];
|
|
129
|
+
const danglingRoots: HarnessRootRegistryForGc["roots"] = [];
|
|
130
|
+
for (const entry of registry.roots) {
|
|
131
|
+
const sessionDir = sessionPaths(entry.root, registry.sessionId).dir;
|
|
132
|
+
if (await exists(sessionDir)) liveRoots.push(entry);
|
|
133
|
+
else danglingRoots.push(entry);
|
|
134
|
+
}
|
|
135
|
+
return { liveRoots, danglingRoots };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const registryEntriesGcAdapter: GcStoreAdapter = {
|
|
139
|
+
store: "registry_entries",
|
|
140
|
+
async collect(ctx: GcContext): Promise<GcCollectResult> {
|
|
141
|
+
const records: GcRecord[] = [];
|
|
142
|
+
const errors: GcError[] = [];
|
|
143
|
+
const registries = await collectRegistries(ctx);
|
|
144
|
+
errors.push(...registryErrors(registries));
|
|
145
|
+
|
|
146
|
+
for (const registry of registries) {
|
|
147
|
+
if (registry.error) continue;
|
|
148
|
+
try {
|
|
149
|
+
const { liveRoots, danglingRoots } = await splitRegistryRoots(registry);
|
|
150
|
+
if (danglingRoots.length === 0) continue;
|
|
151
|
+
records.push({
|
|
152
|
+
store: "registry_entries",
|
|
153
|
+
id: registry.sessionId,
|
|
154
|
+
path: registry.file,
|
|
155
|
+
pid_status: "none",
|
|
156
|
+
status: "dangling",
|
|
157
|
+
stale: true,
|
|
158
|
+
removable: true,
|
|
159
|
+
action: "none",
|
|
160
|
+
reason: `dangling roots: ${danglingRoots.map(entry => entry.root).join(", ")}`,
|
|
161
|
+
detail: `${danglingRoots.length} dangling, ${liveRoots.length} live`,
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
errors.push({ store: "registry_entries", scope: registry.file, message: errorMessage(error) });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { records, errors };
|
|
169
|
+
},
|
|
170
|
+
async prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome> {
|
|
171
|
+
if (!record.path) return { removed: false, skipped: "missing_registry_path" };
|
|
172
|
+
const registries = await collectRegistries(ctx);
|
|
173
|
+
const registry = registries.find(entry => entry.file === record.path);
|
|
174
|
+
if (!registry || registry.error) return { removed: false, skipped: "registry_not_readable" };
|
|
175
|
+
const { liveRoots, danglingRoots } = await splitRegistryRoots(registry);
|
|
176
|
+
if (danglingRoots.length === 0) return { removed: false, skipped: "no_dangling_roots" };
|
|
177
|
+
if (liveRoots.length === 0) {
|
|
178
|
+
await removeHarnessRootRegistryFileForGc(record.path);
|
|
179
|
+
} else {
|
|
180
|
+
await rewriteHarnessRootRegistryForGc(record.path, { sessionId: registry.sessionId, roots: liveRoots });
|
|
181
|
+
}
|
|
182
|
+
return { removed: true };
|
|
183
|
+
},
|
|
184
|
+
};
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { execFileSync } from "node:child_process";
|
|
15
15
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
16
16
|
import { existsSync } from "node:fs";
|
|
17
|
+
import type { AgentWireOwnerObservation } from "../modes/shared/agent-wire/event-contract";
|
|
17
18
|
import { observeRpcOutboundFrame } from "../modes/shared/agent-wire/event-observation";
|
|
18
19
|
import { classifyRecovery } from "./classifier";
|
|
19
20
|
import { ControlServer, type EndpointRequest } from "./control-endpoint";
|
|
@@ -106,7 +107,7 @@ export class RuntimeOwner {
|
|
|
106
107
|
#server: ControlServer;
|
|
107
108
|
#cursor = 0;
|
|
108
109
|
#leaseEpoch = 0;
|
|
109
|
-
#heartbeatTimer:
|
|
110
|
+
#heartbeatTimer: NodeJS.Timeout | null = null;
|
|
110
111
|
#socketPath: string;
|
|
111
112
|
#finalizeChecks?: FinalizeChecks;
|
|
112
113
|
#validationCommands?: ValidationCommandSpec[];
|
|
@@ -199,7 +200,7 @@ export class RuntimeOwner {
|
|
|
199
200
|
await this.#emit("info", "rpc_activity", { coalescedFrames });
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
async #emitMapped(mapped:
|
|
203
|
+
async #emitMapped(mapped: AgentWireOwnerObservation): Promise<void> {
|
|
203
204
|
if (mapped.kind === "rpc_agent_completed") {
|
|
204
205
|
const state = await readSessionState(this.#opts.root, this.#opts.sessionId);
|
|
205
206
|
if (
|
|
@@ -670,3 +671,14 @@ export async function resolveOwner(root: string, sessionId: string): Promise<Res
|
|
|
670
671
|
const live = status === "live" || status === "expiredAlive" || status === "epermAlive";
|
|
671
672
|
return { live, socketPath: lease.endpoint?.path ?? null, lease };
|
|
672
673
|
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Owner liveness for verbs that do not route to the owner (e.g. `classify`): a routable owner
|
|
677
|
+
* has a live lease and a socket endpoint. This is the same lease/socket probe `observe` uses to
|
|
678
|
+
* decide routing, so non-routing verbs derive `ownerLive` consistently instead of assuming the
|
|
679
|
+
* owner is gone (which would misclassify a live owner as vanished/restart-clean).
|
|
680
|
+
*/
|
|
681
|
+
export async function resolveOwnerLive(root: string, sessionId: string): Promise<boolean> {
|
|
682
|
+
const owner = await resolveOwner(root, sessionId);
|
|
683
|
+
return owner.live && owner.socketPath !== null;
|
|
684
|
+
}
|
|
@@ -120,7 +120,7 @@ export class GajaeCodeRpc implements HarnessRpc {
|
|
|
120
120
|
#waiters: {
|
|
121
121
|
afterCursor: number;
|
|
122
122
|
resolve: (v: { cursor: number } | null) => void;
|
|
123
|
-
timer:
|
|
123
|
+
timer: NodeJS.Timeout;
|
|
124
124
|
}[] = [];
|
|
125
125
|
#frameListeners: ((frame: Record<string, unknown>) => void)[] = [];
|
|
126
126
|
#lastFrameAt: string | null = null;
|
|
@@ -32,6 +32,18 @@ interface HarnessRootRegistry {
|
|
|
32
32
|
roots: HarnessRootRegistryEntry[];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export interface HarnessRootRegistryForGc {
|
|
36
|
+
sessionId: string;
|
|
37
|
+
roots: HarnessRootRegistryEntry[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface HarnessRootRegistryListingForGc {
|
|
41
|
+
sessionId: string;
|
|
42
|
+
file: string;
|
|
43
|
+
roots: HarnessRootRegistryEntry[];
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
interface ResolveHarnessSessionRootOptions {
|
|
36
48
|
expectedWorkspace?: string;
|
|
37
49
|
}
|
|
@@ -101,6 +113,64 @@ async function writeHarnessRootRegistry(
|
|
|
101
113
|
const file = harnessRootRegistryPath(registry.sessionId, env);
|
|
102
114
|
await writeJsonAtomicPrivate(file, registry);
|
|
103
115
|
}
|
|
116
|
+
|
|
117
|
+
function parseHarnessRootRegistryForGc(value: unknown, fallbackSessionId: string): HarnessRootRegistryForGc | null {
|
|
118
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
119
|
+
const registry = value as Record<string, unknown>;
|
|
120
|
+
if (typeof registry.sessionId !== "string" || !Array.isArray(registry.roots)) return null;
|
|
121
|
+
const roots: HarnessRootRegistryEntry[] = [];
|
|
122
|
+
for (const entry of registry.roots) {
|
|
123
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
124
|
+
const rootEntry = entry as Record<string, unknown>;
|
|
125
|
+
if (typeof rootEntry.root !== "string" || typeof rootEntry.updatedAt !== "string") return null;
|
|
126
|
+
roots.push({ root: rootEntry.root, updatedAt: rootEntry.updatedAt });
|
|
127
|
+
}
|
|
128
|
+
return { sessionId: registry.sessionId || fallbackSessionId, roots };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @internal */
|
|
132
|
+
export async function listHarnessRootRegistriesForGc(
|
|
133
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
134
|
+
): Promise<HarnessRootRegistryListingForGc[]> {
|
|
135
|
+
const dir = harnessRootRegistryDir(env);
|
|
136
|
+
let entries: string[];
|
|
137
|
+
try {
|
|
138
|
+
entries = await fs.readdir(dir);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
141
|
+
if (code === "ENOENT") return [];
|
|
142
|
+
return [{ sessionId: "", file: dir, roots: [], error: (error as Error).message }];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const registries: HarnessRootRegistryListingForGc[] = [];
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (!entry.endsWith(".json")) continue;
|
|
148
|
+
const file = path.join(dir, entry);
|
|
149
|
+
const fallbackSessionId = entry.slice(0, -".json".length);
|
|
150
|
+
try {
|
|
151
|
+
const raw = await fs.readFile(file, "utf8");
|
|
152
|
+
const parsed = parseHarnessRootRegistryForGc(JSON.parse(raw), fallbackSessionId);
|
|
153
|
+
if (!parsed) {
|
|
154
|
+
registries.push({ sessionId: fallbackSessionId, file, roots: [], error: "malformed_registry" });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
registries.push({ sessionId: parsed.sessionId, file, roots: parsed.roots });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
registries.push({ sessionId: fallbackSessionId, file, roots: [], error: (error as Error).message });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return registries;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** @internal */
|
|
166
|
+
export async function rewriteHarnessRootRegistryForGc(file: string, registry: HarnessRootRegistryForGc): Promise<void> {
|
|
167
|
+
await writeJsonAtomicPrivate(file, registry);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** @internal */
|
|
171
|
+
export async function removeHarnessRootRegistryFileForGc(file: string): Promise<void> {
|
|
172
|
+
await fs.rm(file, { force: true });
|
|
173
|
+
}
|
|
104
174
|
const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
105
175
|
export const MAX_UNIX_SOCKET_PATH_BYTES = 100;
|
|
106
176
|
|
package/src/hooks/skill-state.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { logger } from "@gajae-code/utils";
|
|
2
3
|
import type { SkillDiscoverySettings } from "../config/skill-settings-defaults";
|
|
3
4
|
import { ModeStateSchema, SkillActiveStateSchema } from "../gjc-runtime/state-schema";
|
|
4
5
|
import { writeJsonAtomic, writeWorkflowEnvelopeAtomic } from "../gjc-runtime/state-writer";
|
|
5
6
|
import { isUltragoalBypassPrompt, readUltragoalVerificationState } from "../gjc-runtime/ultragoal-guard";
|
|
7
|
+
import { getUltragoalRunCompletionState, readUltragoalPlan } from "../gjc-runtime/ultragoal-runtime";
|
|
6
8
|
import { buildSessionContext, loadEntriesFromFile, type SessionEntry } from "../session/session-manager";
|
|
7
9
|
import {
|
|
8
10
|
readVisibleSkillActiveState as readCanonicalVisibleSkillActiveState,
|
|
@@ -221,7 +223,7 @@ function skillStatePath(stateDir: string, sessionId?: string): string {
|
|
|
221
223
|
}
|
|
222
224
|
|
|
223
225
|
function warnInvalidState(kind: string, filePath: string, error: string): void {
|
|
224
|
-
|
|
226
|
+
logger.warn(`gjc skill-state: invalid ${kind} at ${filePath}: ${error}`);
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
async function readValidatedJsonFile<T>(
|
|
@@ -480,6 +482,85 @@ function modeStateReleasesStop(state: ModeState | null, handoffRequired: boolean
|
|
|
480
482
|
return false;
|
|
481
483
|
}
|
|
482
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Cross-file coherence guard for a mode-state that claims it releases the Stop
|
|
487
|
+
* block. `modeStateReleasesStop` trusts a single mode-state file; if any writer
|
|
488
|
+
* leaves that file stale or incoherent (e.g. `active:false` / a terminal phase
|
|
489
|
+
* after a `clear` while a new run's goals are still pending), trusting it alone
|
|
490
|
+
* silently defeats the Stop protection.
|
|
491
|
+
*
|
|
492
|
+
* This consults the authoritative durable state the Stop hook can already read
|
|
493
|
+
* and returns a block reason when that state contradicts the release. It stays
|
|
494
|
+
* cheap and read-only — ultragoal reads the durable plan; skills without an
|
|
495
|
+
* independent durable source release as before.
|
|
496
|
+
*/
|
|
497
|
+
async function detectStaleModeStateRelease(skill: GjcWorkflowSkill, cwd: string): Promise<string | null> {
|
|
498
|
+
if (skill === "ultragoal") {
|
|
499
|
+
const plan = await readUltragoalPlan(cwd);
|
|
500
|
+
if (!plan) return null;
|
|
501
|
+
const runState = getUltragoalRunCompletionState(plan);
|
|
502
|
+
if (runState.incompleteGoals.length > 0) {
|
|
503
|
+
return `the durable Ultragoal plan still has incomplete required goals (${runState.incompleteGoals
|
|
504
|
+
.map(goal => goal.id)
|
|
505
|
+
.join(", ")}); run \`gjc ultragoal complete-goals\` to continue`;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Deep-interview terminal phases that represent an explicit abort/cancel rather
|
|
513
|
+
* than an ordinary stop. These are legitimate terminals even without a
|
|
514
|
+
* crystallized spec, so they must NOT be forced through crystallization.
|
|
515
|
+
*/
|
|
516
|
+
const DEEP_INTERVIEW_ABORT_PHASES = new Set(["failed", "cancelled", "canceled"]);
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* A deep-interview run is "crystallized" once it has persisted a final spec.
|
|
520
|
+
* `persistDeepInterviewSpec` records the spec path in the mode-state and writes
|
|
521
|
+
* the artifact under `.gjc/specs/`, so a crystallized state carries a
|
|
522
|
+
* `spec_path` that still resolves to a real file. A bare `spec_path` with no
|
|
523
|
+
* backing file (deleted/stale/fabricated) does not count as crystallized.
|
|
524
|
+
*/
|
|
525
|
+
async function deepInterviewSpecCrystallized(state: ModeState, cwd: string): Promise<boolean> {
|
|
526
|
+
const raw = state.spec_path;
|
|
527
|
+
const specPath = typeof raw === "string" ? raw.trim() : "";
|
|
528
|
+
if (!specPath) return false;
|
|
529
|
+
const resolved = path.isAbsolute(specPath) ? specPath : path.resolve(cwd, specPath);
|
|
530
|
+
try {
|
|
531
|
+
return await Bun.file(resolved).exists();
|
|
532
|
+
} catch {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Deep-interview-scoped terminalization guard (#674). An ordinary stop must not
|
|
539
|
+
* let a deep-interview run disappear as a generic stopped task while the user
|
|
540
|
+
* still needs the distilled interview state: when its mode-state would release
|
|
541
|
+
* the Stop block it must have actually crystallized the interview into a
|
|
542
|
+
* persisted spec/handoff. Explicit abort/cancel phases and the `active:false`
|
|
543
|
+
* demotion/clear outcome (the handoff/chain result) remain legitimate terminals.
|
|
544
|
+
* Returns a public-safe diagnostic that forces crystallization, or null to
|
|
545
|
+
* release. Scoped to deep-interview only — other workflows are untouched.
|
|
546
|
+
*/
|
|
547
|
+
async function detectUncrystallizedDeepInterviewStop(
|
|
548
|
+
skill: GjcWorkflowSkill,
|
|
549
|
+
state: ModeState | null,
|
|
550
|
+
cwd: string,
|
|
551
|
+
): Promise<string | null> {
|
|
552
|
+
if (skill !== "deep-interview") return null;
|
|
553
|
+
// active:false is the demotion/clear outcome (chain handoff or explicit
|
|
554
|
+
// clear already terminalized the run); a missing state blocks upstream.
|
|
555
|
+
if (state?.active !== true) return null;
|
|
556
|
+
const phase = String(state.current_phase ?? "")
|
|
557
|
+
.trim()
|
|
558
|
+
.toLowerCase();
|
|
559
|
+
if (DEEP_INTERVIEW_ABORT_PHASES.has(phase)) return null;
|
|
560
|
+
if (await deepInterviewSpecCrystallized(state, cwd)) return null;
|
|
561
|
+
return `the deep-interview run reached a terminal phase ("${phase || "unknown"}") without crystallizing a usable spec/handoff. Run \`gjc deep-interview --write --stage final\` (optionally \`--handoff ralplan\`) to persist the distilled interview spec, hand off through the deep-interview policy, or explicitly cancel/clear the interview before stopping`;
|
|
562
|
+
}
|
|
563
|
+
|
|
483
564
|
async function readVisibleModeState(
|
|
484
565
|
cwd: string,
|
|
485
566
|
skill: GjcWorkflowSkill,
|
|
@@ -572,7 +653,45 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
|
|
|
572
653
|
ModeStateSchema,
|
|
573
654
|
);
|
|
574
655
|
const handoffRequired = isHandoffRequiredSkill(entry.skill);
|
|
575
|
-
if (modeStateReleasesStop(modeState, handoffRequired))
|
|
656
|
+
if (modeStateReleasesStop(modeState, handoffRequired)) {
|
|
657
|
+
// A mode-state that claims it releases the Stop block must agree with
|
|
658
|
+
// authoritative durable state. If a stale/incoherent mode-state would
|
|
659
|
+
// release while the plan/ledger still shows pending work, block instead
|
|
660
|
+
// of trusting the single file (see #659).
|
|
661
|
+
const staleRelease = await detectStaleModeStateRelease(entry.skill, input.cwd);
|
|
662
|
+
if (staleRelease) {
|
|
663
|
+
const coherenceMessage = `GJC skill "${entry.skill}" mode-state reports it released the Stop block (${modeStatePath(
|
|
664
|
+
resolvedStateDir,
|
|
665
|
+
entry.skill,
|
|
666
|
+
input.sessionId,
|
|
667
|
+
)}), but ${staleRelease}. The mode-state is incoherent with authoritative durable state; finish or explicitly clear the pending work before stopping.`;
|
|
668
|
+
return {
|
|
669
|
+
decision: "block",
|
|
670
|
+
reason: coherenceMessage,
|
|
671
|
+
stopReason: `gjc_skill_${entry.skill.replace(/-/g, "_")}_stale_mode_state`,
|
|
672
|
+
systemMessage: coherenceMessage,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
// Deep-interview must not terminalize through an ordinary stop without
|
|
676
|
+
// crystallizing its distilled interview state into a spec/handoff
|
|
677
|
+
// (explicit abort/cancel and the active:false demotion are preserved
|
|
678
|
+
// as legitimate terminals). See #674.
|
|
679
|
+
const uncrystallized = await detectUncrystallizedDeepInterviewStop(entry.skill, modeState, input.cwd);
|
|
680
|
+
if (uncrystallized) {
|
|
681
|
+
const crystallizeMessage = `GJC deep-interview must crystallize before stopping (${modeStatePath(
|
|
682
|
+
resolvedStateDir,
|
|
683
|
+
entry.skill,
|
|
684
|
+
input.sessionId,
|
|
685
|
+
)}): ${uncrystallized}.`;
|
|
686
|
+
return {
|
|
687
|
+
decision: "block",
|
|
688
|
+
reason: crystallizeMessage,
|
|
689
|
+
stopReason: "gjc_skill_deep_interview_uncrystallized",
|
|
690
|
+
systemMessage: crystallizeMessage,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
576
695
|
const phase = String(modeState?.current_phase ?? entry.phase ?? skillState.phase ?? "active");
|
|
577
696
|
const statePath = modeStatePath(resolvedStateDir, entry.skill, input.sessionId);
|
|
578
697
|
if (entry.skill === "ultragoal") {
|