@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
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gjc gc` runtime — a global, liveness-only, dry-run-by-default garbage
|
|
3
|
+
* collector for stale GJC session/PID records.
|
|
4
|
+
*
|
|
5
|
+
* Design (see .gjc/plans/ralplan/2026-06-13-1347-954f/pending-approval.md):
|
|
6
|
+
* - This module is an ORCHESTRATOR only. It owns the shared PID probe, the
|
|
7
|
+
* report/exit-code policy, and text/JSON rendering. It must NOT parse private
|
|
8
|
+
* store layouts directly; every store is reached through an injectable
|
|
9
|
+
* `GcStoreAdapter` that lives next to its store owner.
|
|
10
|
+
* - Liveness-only and fail-closed: only `ESRCH` (no such process) is `dead`
|
|
11
|
+
* (removable). `process.kill(pid, 0)` success, `EPERM`, and any unknown probe
|
|
12
|
+
* error all mean KEEP — a live process is never signalled or killed.
|
|
13
|
+
* - Dry-run by default: nothing is deleted unless `--prune`/`--force`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { buildGcReportText } from "./gc-render";
|
|
17
|
+
|
|
18
|
+
export type GcStore = "harness_leases" | "team_workers" | "file_locks" | "tmux_sessions" | "registry_entries";
|
|
19
|
+
|
|
20
|
+
export const GC_STORES: readonly GcStore[] = [
|
|
21
|
+
"harness_leases",
|
|
22
|
+
"team_workers",
|
|
23
|
+
"file_locks",
|
|
24
|
+
"tmux_sessions",
|
|
25
|
+
"registry_entries",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
/** Why a probed pid is kept instead of treated as dead. */
|
|
29
|
+
export type GcPidKeepReason = "alive" | "eperm" | "unknown";
|
|
30
|
+
|
|
31
|
+
export interface GcPidProbeResult {
|
|
32
|
+
/** `dead` only on ESRCH; `keep` for alive/eperm/unknown (fail-closed). */
|
|
33
|
+
status: "dead" | "keep";
|
|
34
|
+
reason?: GcPidKeepReason;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Single shared liveness contract threaded through every classifier + prune path. */
|
|
39
|
+
export type GcPidProbe = (pid: number) => GcPidProbeResult;
|
|
40
|
+
|
|
41
|
+
export type GcPidStatus = "dead" | "alive" | "eperm" | "unknown" | "none";
|
|
42
|
+
|
|
43
|
+
export type GcAction = "none" | "would_remove" | "removed" | "remove_failed" | "skipped";
|
|
44
|
+
|
|
45
|
+
export interface GcRecord {
|
|
46
|
+
store: GcStore;
|
|
47
|
+
/** Stable identifier: session id, lock dir path, worker id, tmux name, registry session id. */
|
|
48
|
+
id: string;
|
|
49
|
+
path?: string;
|
|
50
|
+
root?: string;
|
|
51
|
+
pid?: number;
|
|
52
|
+
pid_status?: GcPidStatus;
|
|
53
|
+
/** Store-specific classification label (e.g. "dead", "live", "unclassified", "terminal_lifecycle"). */
|
|
54
|
+
status: string;
|
|
55
|
+
stale: boolean;
|
|
56
|
+
removable: boolean;
|
|
57
|
+
action: GcAction;
|
|
58
|
+
reason: string;
|
|
59
|
+
detail?: string;
|
|
60
|
+
error?: string;
|
|
61
|
+
removed?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface GcError {
|
|
65
|
+
store: GcStore;
|
|
66
|
+
scope: string;
|
|
67
|
+
message: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface GcCollectResult {
|
|
71
|
+
records: GcRecord[];
|
|
72
|
+
errors: GcError[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface GcPruneOutcome {
|
|
76
|
+
removed: boolean;
|
|
77
|
+
error?: string;
|
|
78
|
+
/** Set when a removable record was skipped at prune time (e.g. TOCTOU became live). */
|
|
79
|
+
skipped?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface GcContext {
|
|
83
|
+
probe: GcPidProbe;
|
|
84
|
+
force: boolean;
|
|
85
|
+
env: NodeJS.ProcessEnv;
|
|
86
|
+
cwd: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A store-owned GC adapter. `collect` discovers + classifies (using the shared
|
|
91
|
+
* probe) without mutating anything. `prune` removes a single record, and MUST
|
|
92
|
+
* re-validate / re-probe immediately before any destructive action.
|
|
93
|
+
*/
|
|
94
|
+
export interface GcStoreAdapter {
|
|
95
|
+
store: GcStore;
|
|
96
|
+
collect(ctx: GcContext): Promise<GcCollectResult>;
|
|
97
|
+
prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface GcCounts {
|
|
101
|
+
discovered: number;
|
|
102
|
+
stale: number;
|
|
103
|
+
alive: number;
|
|
104
|
+
eperm: number;
|
|
105
|
+
unknown: number;
|
|
106
|
+
terminal_lifecycle: number;
|
|
107
|
+
unclassified: number;
|
|
108
|
+
would_remove: number;
|
|
109
|
+
removed: number;
|
|
110
|
+
failed: number;
|
|
111
|
+
errors: number;
|
|
112
|
+
by_store: Record<
|
|
113
|
+
GcStore,
|
|
114
|
+
{ discovered: number; stale: number; would_remove: number; removed: number; failed: number }
|
|
115
|
+
>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface GcReport {
|
|
119
|
+
dry_run: boolean;
|
|
120
|
+
stores: Record<GcStore, GcRecord[]>;
|
|
121
|
+
counts: GcCounts;
|
|
122
|
+
errors: GcError[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface GcRunResult {
|
|
126
|
+
stdout: string;
|
|
127
|
+
stderr: string;
|
|
128
|
+
status: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The shared, fail-closed PID probe. ESRCH => dead/removable; success => alive;
|
|
133
|
+
* EPERM => kept (owned by another user); any other error => kept as unknown.
|
|
134
|
+
*/
|
|
135
|
+
export const gcPidProbe: GcPidProbe = (pid: number): GcPidProbeResult => {
|
|
136
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
137
|
+
return { status: "keep", reason: "unknown", error: `invalid_pid:${pid}` };
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
process.kill(pid, 0);
|
|
141
|
+
return { status: "keep", reason: "alive" };
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
144
|
+
if (code === "ESRCH") return { status: "dead" };
|
|
145
|
+
if (code === "EPERM") return { status: "keep", reason: "eperm" };
|
|
146
|
+
return { status: "keep", reason: "unknown", error: code ?? String(error) };
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** Map a `GcPidProbe` onto the harness lease probe shape (`"alive"|"dead"|"eperm"`). */
|
|
151
|
+
export function gcProbeToLeasePidStatus(probe: GcPidProbe): (pid: number) => "alive" | "dead" | "eperm" {
|
|
152
|
+
return (pid: number) => {
|
|
153
|
+
const result = probe(pid);
|
|
154
|
+
if (result.status === "dead") return "dead";
|
|
155
|
+
// EPERM stays eperm; unknown maps to alive so classifyLeaseStatus keeps it.
|
|
156
|
+
return result.reason === "eperm" ? "eperm" : "alive";
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Translate a probe result into a record-friendly pid status label. */
|
|
161
|
+
export function gcPidStatusLabel(result: GcPidProbeResult): Exclude<GcPidStatus, "none"> {
|
|
162
|
+
if (result.status === "dead") return "dead";
|
|
163
|
+
return result.reason ?? "alive";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function emptyByStore(): GcCounts["by_store"] {
|
|
167
|
+
const by = {} as GcCounts["by_store"];
|
|
168
|
+
for (const store of GC_STORES) {
|
|
169
|
+
by[store] = { discovered: 0, stale: 0, would_remove: 0, removed: 0, failed: 0 };
|
|
170
|
+
}
|
|
171
|
+
return by;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function emptyStores(): Record<GcStore, GcRecord[]> {
|
|
175
|
+
const stores = {} as Record<GcStore, GcRecord[]>;
|
|
176
|
+
for (const store of GC_STORES) stores[store] = [];
|
|
177
|
+
return stores;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function computeCounts(stores: Record<GcStore, GcRecord[]>, errors: GcError[]): GcCounts {
|
|
181
|
+
const counts: GcCounts = {
|
|
182
|
+
discovered: 0,
|
|
183
|
+
stale: 0,
|
|
184
|
+
alive: 0,
|
|
185
|
+
eperm: 0,
|
|
186
|
+
unknown: 0,
|
|
187
|
+
terminal_lifecycle: 0,
|
|
188
|
+
unclassified: 0,
|
|
189
|
+
would_remove: 0,
|
|
190
|
+
removed: 0,
|
|
191
|
+
failed: 0,
|
|
192
|
+
errors: errors.length,
|
|
193
|
+
by_store: emptyByStore(),
|
|
194
|
+
};
|
|
195
|
+
for (const store of GC_STORES) {
|
|
196
|
+
for (const record of stores[store]) {
|
|
197
|
+
counts.discovered++;
|
|
198
|
+
counts.by_store[store].discovered++;
|
|
199
|
+
if (record.stale) {
|
|
200
|
+
counts.stale++;
|
|
201
|
+
counts.by_store[store].stale++;
|
|
202
|
+
}
|
|
203
|
+
if (record.pid_status === "alive") counts.alive++;
|
|
204
|
+
else if (record.pid_status === "eperm") counts.eperm++;
|
|
205
|
+
else if (record.pid_status === "unknown") counts.unknown++;
|
|
206
|
+
if (record.status === "terminal_lifecycle") counts.terminal_lifecycle++;
|
|
207
|
+
if (record.status === "unclassified") counts.unclassified++;
|
|
208
|
+
if (record.action === "would_remove") {
|
|
209
|
+
counts.would_remove++;
|
|
210
|
+
counts.by_store[store].would_remove++;
|
|
211
|
+
}
|
|
212
|
+
if (record.action === "removed") {
|
|
213
|
+
counts.removed++;
|
|
214
|
+
counts.by_store[store].removed++;
|
|
215
|
+
}
|
|
216
|
+
if (record.action === "remove_failed") {
|
|
217
|
+
counts.failed++;
|
|
218
|
+
counts.by_store[store].failed++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return counts;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
interface ParsedGcArgs {
|
|
226
|
+
json: boolean;
|
|
227
|
+
prune: boolean;
|
|
228
|
+
help: boolean;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
class GcUsageError extends Error {}
|
|
232
|
+
|
|
233
|
+
function parseGcArgs(argv: string[]): ParsedGcArgs {
|
|
234
|
+
let json = false;
|
|
235
|
+
let prune = false;
|
|
236
|
+
let dryRun = false;
|
|
237
|
+
let help = false;
|
|
238
|
+
for (const arg of argv) {
|
|
239
|
+
switch (arg) {
|
|
240
|
+
case "--json":
|
|
241
|
+
case "-j":
|
|
242
|
+
json = true;
|
|
243
|
+
break;
|
|
244
|
+
case "--prune":
|
|
245
|
+
case "--force":
|
|
246
|
+
prune = true;
|
|
247
|
+
break;
|
|
248
|
+
case "--dry-run":
|
|
249
|
+
dryRun = true;
|
|
250
|
+
break;
|
|
251
|
+
case "--help":
|
|
252
|
+
case "-h":
|
|
253
|
+
help = true;
|
|
254
|
+
break;
|
|
255
|
+
default:
|
|
256
|
+
throw new GcUsageError(`unknown_flag:${arg}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Explicit --dry-run always wins over --prune/--force.
|
|
260
|
+
if (dryRun) prune = false;
|
|
261
|
+
return { json, prune, help };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Collect every store's records (catching hard discovery errors per adapter),
|
|
266
|
+
* then optionally prune removable records with per-record revalidation.
|
|
267
|
+
*/
|
|
268
|
+
export async function collectGcReport(adapters: GcStoreAdapter[], ctx: GcContext, prune: boolean): Promise<GcReport> {
|
|
269
|
+
const stores = emptyStores();
|
|
270
|
+
const errors: GcError[] = [];
|
|
271
|
+
|
|
272
|
+
for (const adapter of adapters) {
|
|
273
|
+
try {
|
|
274
|
+
const result = await adapter.collect(ctx);
|
|
275
|
+
stores[adapter.store].push(...result.records);
|
|
276
|
+
errors.push(...result.errors);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
errors.push({
|
|
279
|
+
store: adapter.store,
|
|
280
|
+
scope: "collect",
|
|
281
|
+
message: error instanceof Error ? error.message : String(error),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Mark dry-run intent on every removable record before pruning.
|
|
287
|
+
for (const store of GC_STORES) {
|
|
288
|
+
for (const record of stores[store]) {
|
|
289
|
+
if (record.removable) record.action = "would_remove";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (prune) {
|
|
294
|
+
const adapterByStore = new Map(adapters.map(a => [a.store, a] as const));
|
|
295
|
+
for (const store of GC_STORES) {
|
|
296
|
+
const adapter = adapterByStore.get(store);
|
|
297
|
+
if (!adapter) continue;
|
|
298
|
+
for (const record of stores[store]) {
|
|
299
|
+
if (!record.removable) continue;
|
|
300
|
+
try {
|
|
301
|
+
const outcome = await adapter.prune(record, ctx);
|
|
302
|
+
if (outcome.removed) {
|
|
303
|
+
record.action = "removed";
|
|
304
|
+
record.removed = true;
|
|
305
|
+
} else if (outcome.skipped) {
|
|
306
|
+
record.action = "skipped";
|
|
307
|
+
record.reason = outcome.skipped;
|
|
308
|
+
record.removed = false;
|
|
309
|
+
} else {
|
|
310
|
+
record.action = "remove_failed";
|
|
311
|
+
record.removed = false;
|
|
312
|
+
record.error = outcome.error ?? "remove_failed";
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
record.action = "remove_failed";
|
|
316
|
+
record.removed = false;
|
|
317
|
+
record.error = error instanceof Error ? error.message : String(error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return { dry_run: !prune, stores, counts: computeCounts(stores, errors), errors };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Exit-code policy:
|
|
328
|
+
* - usage/parse error => 2
|
|
329
|
+
* - hard discovery errors => 1 (both modes)
|
|
330
|
+
* - prune mode with a failed intended removal => 1
|
|
331
|
+
* - otherwise => 0
|
|
332
|
+
*/
|
|
333
|
+
export function computeExitCode(report: GcReport): number {
|
|
334
|
+
if (report.errors.length > 0) return 1;
|
|
335
|
+
if (!report.dry_run && report.counts.failed > 0) return 1;
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export async function runGjcGcCommand(
|
|
340
|
+
argv: string[],
|
|
341
|
+
cwd: string = process.cwd(),
|
|
342
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
343
|
+
adapters?: GcStoreAdapter[],
|
|
344
|
+
): Promise<GcRunResult> {
|
|
345
|
+
let parsed: ParsedGcArgs;
|
|
346
|
+
try {
|
|
347
|
+
parsed = parseGcArgs(argv);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
const message = error instanceof GcUsageError ? error.message : String(error);
|
|
350
|
+
return { stdout: "", stderr: `gjc gc: ${message}\n`, status: 2 };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (parsed.help) {
|
|
354
|
+
return { stdout: gcHelpText(), stderr: "", status: 0 };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const resolvedAdapters = adapters ?? (await defaultGcAdapters());
|
|
358
|
+
const ctx: GcContext = { probe: gcPidProbe, force: parsed.prune, env, cwd };
|
|
359
|
+
const report = await collectGcReport(resolvedAdapters, ctx, parsed.prune);
|
|
360
|
+
const status = computeExitCode(report);
|
|
361
|
+
const stdout = parsed.json ? `${JSON.stringify(report, null, 2)}\n` : buildGcReportText(report);
|
|
362
|
+
return { stdout, stderr: "", status };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function gcHelpText(): string {
|
|
366
|
+
return [
|
|
367
|
+
"gjc gc - garbage-collect stale GJC session/PID records",
|
|
368
|
+
"",
|
|
369
|
+
"USAGE",
|
|
370
|
+
" $ gjc gc [--prune|--force] [--json]",
|
|
371
|
+
"",
|
|
372
|
+
"FLAGS",
|
|
373
|
+
" --prune, --force Actually remove stale records (default: dry-run report only)",
|
|
374
|
+
" --dry-run Force report-only mode (overrides --prune/--force)",
|
|
375
|
+
" -j, --json Emit machine-readable JSON",
|
|
376
|
+
"",
|
|
377
|
+
"Liveness-only: a record is removed only when its owning process is dead",
|
|
378
|
+
"(ESRCH). Live / permission-denied / unknown processes are always kept.",
|
|
379
|
+
"",
|
|
380
|
+
].join("\n");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Lazily assemble the real store adapters (kept lazy to avoid import cycles). */
|
|
384
|
+
export async function defaultGcAdapters(): Promise<GcStoreAdapter[]> {
|
|
385
|
+
const [
|
|
386
|
+
{ harnessLeasesGcAdapter, registryEntriesGcAdapter },
|
|
387
|
+
{ fileLocksGcAdapter },
|
|
388
|
+
{ teamWorkersGcAdapter },
|
|
389
|
+
{ tmuxSessionsGcAdapter },
|
|
390
|
+
] = await Promise.all([
|
|
391
|
+
import("../harness-control-plane/gc-adapter"),
|
|
392
|
+
import("../config/file-lock-gc"),
|
|
393
|
+
import("./team-gc"),
|
|
394
|
+
import("./tmux-gc"),
|
|
395
|
+
]);
|
|
396
|
+
return [
|
|
397
|
+
harnessLeasesGcAdapter,
|
|
398
|
+
teamWorkersGcAdapter,
|
|
399
|
+
fileLocksGcAdapter,
|
|
400
|
+
tmuxSessionsGcAdapter,
|
|
401
|
+
registryEntriesGcAdapter,
|
|
402
|
+
];
|
|
403
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { safeStderrWrite } from "@gajae-code/utils";
|
|
2
3
|
import type { Args } from "../cli/args";
|
|
3
4
|
import {
|
|
4
5
|
buildGjcTmuxProfileCommands,
|
|
@@ -280,7 +281,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
280
281
|
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
281
282
|
const failure =
|
|
282
283
|
profile.failures.find(item => item.command.args.includes("@gjc-profile")) ?? profile.failures[0];
|
|
283
|
-
(context.diagnosticWriter ??
|
|
284
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
284
285
|
formatTmuxLaunchDiagnostic("profile tagging failed", failure?.stderr),
|
|
285
286
|
);
|
|
286
287
|
return true;
|
|
@@ -289,8 +290,6 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
289
290
|
if (created.exitCode !== 0) return false;
|
|
290
291
|
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
|
|
291
292
|
if (attached.exitCode === 0) return true;
|
|
292
|
-
(context.diagnosticWriter ??
|
|
293
|
-
formatTmuxLaunchDiagnostic("attach failed", attached.stderr),
|
|
294
|
-
);
|
|
293
|
+
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
|
|
295
294
|
return true;
|
|
296
295
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parse + summarize for ledger-backed skill observability.
|
|
3
|
+
*
|
|
4
|
+
* Workflow progress for ultragoal/ralplan cannot be observed via subagent tool
|
|
5
|
+
* events (those skills persist through `bash`-backed `gjc` CLI calls whose tool
|
|
6
|
+
* `details` carry no structured payload). The durable source of truth is the
|
|
7
|
+
* append-only ledgers:
|
|
8
|
+
* - ultragoal: `.gjc/ultragoal/ledger.jsonl`
|
|
9
|
+
* - ralplan: `.gjc/plans/ralplan/<run-id>/index.jsonl`
|
|
10
|
+
*
|
|
11
|
+
* This module is I/O-free: callers read the files and pass lines or already-parsed
|
|
12
|
+
* rows. It feeds the compact HUD chip builders in `skill-state/workflow-hud.ts`
|
|
13
|
+
* via the runtime sync paths. Display-string helpers stay theme-free.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/* ------------------------------- ultragoal ------------------------------- */
|
|
17
|
+
|
|
18
|
+
/** Minimal projection of an ultragoal ledger row used for the HUD chip. */
|
|
19
|
+
export interface UltragoalLedgerEventLite {
|
|
20
|
+
/** Normalized from the row's `event` field, or `type` for reconcile rows. */
|
|
21
|
+
event: string;
|
|
22
|
+
goalId?: string;
|
|
23
|
+
status?: string;
|
|
24
|
+
timestamp?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Coerce an already-parsed ledger row into the lite shape. Accepts both the
|
|
29
|
+
* `event`-keyed vocabulary (plan_created, goal_started, goal_checkpointed,
|
|
30
|
+
* steering_accepted/rejected, review_blockers_recorded) and the `type`-keyed
|
|
31
|
+
* reconcile-failure row (`type: "reconcile_failed"`). Returns undefined when no
|
|
32
|
+
* event/type discriminator is present.
|
|
33
|
+
*/
|
|
34
|
+
export function coerceUltragoalLedgerEvent(row: Record<string, unknown>): UltragoalLedgerEventLite | undefined {
|
|
35
|
+
const event = typeof row.event === "string" ? row.event : typeof row.type === "string" ? row.type : undefined;
|
|
36
|
+
if (!event) return undefined;
|
|
37
|
+
const lite: UltragoalLedgerEventLite = { event };
|
|
38
|
+
if (typeof row.goalId === "string") lite.goalId = row.goalId;
|
|
39
|
+
if (typeof row.status === "string") lite.status = row.status;
|
|
40
|
+
if (typeof row.timestamp === "string") lite.timestamp = row.timestamp;
|
|
41
|
+
return lite;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Parse a single ultragoal ledger JSONL line; undefined for blank/malformed lines. */
|
|
45
|
+
export function parseUltragoalLedgerLine(line: string): UltragoalLedgerEventLite | undefined {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (!trimmed) return undefined;
|
|
48
|
+
let row: unknown;
|
|
49
|
+
try {
|
|
50
|
+
row = JSON.parse(trimmed);
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return undefined;
|
|
55
|
+
return coerceUltragoalLedgerEvent(row as Record<string, unknown>);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The most recent event, or undefined when the ledger is empty. */
|
|
59
|
+
export function latestUltragoalLedgerEvent(
|
|
60
|
+
events: readonly UltragoalLedgerEventLite[],
|
|
61
|
+
): UltragoalLedgerEventLite | undefined {
|
|
62
|
+
return events.length > 0 ? events[events.length - 1] : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Best-effort latest event from raw ledger text: parses line-by-line and skips
|
|
67
|
+
* blank/malformed rows so a torn or hand-edited ledger never throws on the HUD
|
|
68
|
+
* path. Strict receipt consumers should keep using the validating reader.
|
|
69
|
+
*/
|
|
70
|
+
export function latestUltragoalLedgerEventFromText(text: string): UltragoalLedgerEventLite | undefined {
|
|
71
|
+
const events: UltragoalLedgerEventLite[] = [];
|
|
72
|
+
for (const line of text.split(/\r?\n/)) {
|
|
73
|
+
const event = parseUltragoalLedgerLine(line);
|
|
74
|
+
if (event) events.push(event);
|
|
75
|
+
}
|
|
76
|
+
return latestUltragoalLedgerEvent(events);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* -------------------------------- ralplan -------------------------------- */
|
|
80
|
+
|
|
81
|
+
/** Minimal projection of a ralplan `index.jsonl` row. */
|
|
82
|
+
export interface RalplanIndexRow {
|
|
83
|
+
stage: string;
|
|
84
|
+
stageN?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Stages that open a new consensus iteration when they appear in append order. */
|
|
88
|
+
const RALPLAN_ITERATION_OPENERS = new Set(["planner", "revision"]);
|
|
89
|
+
|
|
90
|
+
const RALPLAN_STAGE_CODES: Record<string, string> = {
|
|
91
|
+
planner: "P",
|
|
92
|
+
revision: "R",
|
|
93
|
+
architect: "A",
|
|
94
|
+
critic: "C",
|
|
95
|
+
adr: "D",
|
|
96
|
+
final: "F",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const DEFAULT_STAGE_PRESENCE_CAP = 6;
|
|
100
|
+
|
|
101
|
+
/** Parse a single ralplan index JSONL line; undefined for blank/malformed lines. */
|
|
102
|
+
export function parseRalplanIndexLine(line: string): RalplanIndexRow | undefined {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (!trimmed) return undefined;
|
|
105
|
+
let row: unknown;
|
|
106
|
+
try {
|
|
107
|
+
row = JSON.parse(trimmed);
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return undefined;
|
|
112
|
+
const record = row as Record<string, unknown>;
|
|
113
|
+
if (typeof record.stage !== "string") return undefined;
|
|
114
|
+
const out: RalplanIndexRow = { stage: record.stage };
|
|
115
|
+
if (typeof record.stage_n === "number") out.stageN = record.stage_n;
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface RalplanIndexSummary {
|
|
120
|
+
/** Number of consensus iterations (planner/revision boundaries), >= 0. */
|
|
121
|
+
iteration: number;
|
|
122
|
+
/** Stage names present in the current (latest) iteration, in append order. */
|
|
123
|
+
currentStages: string[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Derive iteration count and current-iteration stage presence from index rows.
|
|
128
|
+
*
|
|
129
|
+
* `stage_n` is NOT used as the iteration key: it is stored verbatim per row and a
|
|
130
|
+
* single planner/architect/critic pass can span multiple stage_n values. Instead,
|
|
131
|
+
* a `planner` or `revision` row opens a new iteration and subsequent rows attach
|
|
132
|
+
* to it. No verdict is derived here (index rows carry none).
|
|
133
|
+
*/
|
|
134
|
+
export function summarizeRalplanIndex(rows: readonly RalplanIndexRow[]): RalplanIndexSummary {
|
|
135
|
+
let iteration = 0;
|
|
136
|
+
let currentStages: string[] = [];
|
|
137
|
+
for (const row of rows) {
|
|
138
|
+
if (RALPLAN_ITERATION_OPENERS.has(row.stage)) {
|
|
139
|
+
iteration += 1;
|
|
140
|
+
currentStages = [row.stage];
|
|
141
|
+
} else {
|
|
142
|
+
if (iteration === 0) iteration = 1;
|
|
143
|
+
currentStages.push(row.stage);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { iteration, currentStages };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Compact, theme-free presence string for the ralplan `stages` chip, e.g.
|
|
151
|
+
* `P·A·C`. Collapses past `cap` with a "… N more" suffix. Returns undefined when
|
|
152
|
+
* there are no stages.
|
|
153
|
+
*/
|
|
154
|
+
export function formatRalplanStagePresence(
|
|
155
|
+
stages: readonly string[],
|
|
156
|
+
cap = DEFAULT_STAGE_PRESENCE_CAP,
|
|
157
|
+
): string | undefined {
|
|
158
|
+
if (stages.length === 0) return undefined;
|
|
159
|
+
const codes = stages.map(stage => RALPLAN_STAGE_CODES[stage] ?? stage.charAt(0).toUpperCase());
|
|
160
|
+
if (codes.length <= cap) return codes.join("·");
|
|
161
|
+
const shown = codes.slice(0, cap).join("·");
|
|
162
|
+
const remaining = codes.length - cap;
|
|
163
|
+
return `${shown} … ${remaining} more ${remaining === 1 ? "stage" : "stages"}`;
|
|
164
|
+
}
|