@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
|
@@ -4,10 +4,30 @@ import * as path from "node:path";
|
|
|
4
4
|
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
5
5
|
import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
|
|
6
6
|
import { applyGjcTmuxProfile } from "./launch-tmux";
|
|
7
|
+
import {
|
|
8
|
+
AlreadyExistsError,
|
|
9
|
+
appendJsonl as appendJsonlAudited,
|
|
10
|
+
appendText,
|
|
11
|
+
createJsonNoClobber,
|
|
12
|
+
deleteIfOwned,
|
|
13
|
+
removeFileAudited,
|
|
14
|
+
writeJsonAtomic,
|
|
15
|
+
writeReport,
|
|
16
|
+
} from "./state-writer";
|
|
17
|
+
import { GJC_TMUX_PROFILE_OPTION, GJC_TMUX_PROFILE_VALUE } from "./tmux-common";
|
|
7
18
|
|
|
8
19
|
export type GjcTeamPhase = "starting" | "running" | "awaiting_integration" | "complete" | "failed" | "cancelled";
|
|
9
20
|
export type GjcTeamTaskStatus = "pending" | "blocked" | "in_progress" | "completed" | "failed";
|
|
10
21
|
export type GjcWorkerStatusState = "idle" | "working" | "blocked" | "done" | "failed" | "draining" | "unknown";
|
|
22
|
+
export type GjcTeamWorkerLifecycleState =
|
|
23
|
+
| "starting"
|
|
24
|
+
| "ready"
|
|
25
|
+
| "working"
|
|
26
|
+
| "draining"
|
|
27
|
+
| "stopped"
|
|
28
|
+
| "failed"
|
|
29
|
+
| "unknown";
|
|
30
|
+
export type GjcTeamShutdownMode = "graceful" | "force" | "abort";
|
|
11
31
|
|
|
12
32
|
export const GJC_TEAM_DEFAULT_WORKERS = 3;
|
|
13
33
|
export const GJC_TEAM_MAX_WORKERS = 20;
|
|
@@ -47,6 +67,27 @@ export interface GjcTeamTaskClaim {
|
|
|
47
67
|
token: string;
|
|
48
68
|
leased_until: string;
|
|
49
69
|
}
|
|
70
|
+
export type GjcTeamTaskCompletionEvidenceKind = "command" | "inspection" | "artifact";
|
|
71
|
+
export type GjcTeamTaskCompletionEvidenceStatus = "passed" | "failed" | "not_run" | "verified" | "rejected";
|
|
72
|
+
|
|
73
|
+
export interface GjcTeamTaskCompletionEvidenceItem {
|
|
74
|
+
kind: GjcTeamTaskCompletionEvidenceKind;
|
|
75
|
+
status: GjcTeamTaskCompletionEvidenceStatus;
|
|
76
|
+
summary: string;
|
|
77
|
+
command?: string;
|
|
78
|
+
artifact?: string;
|
|
79
|
+
location?: string;
|
|
80
|
+
output?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface GjcTeamTaskCompletionEvidence {
|
|
84
|
+
summary: string;
|
|
85
|
+
items: GjcTeamTaskCompletionEvidenceItem[];
|
|
86
|
+
files?: string[];
|
|
87
|
+
notes?: string;
|
|
88
|
+
recorded_by: string;
|
|
89
|
+
recorded_at: string;
|
|
90
|
+
}
|
|
50
91
|
|
|
51
92
|
export interface GjcTeamTask {
|
|
52
93
|
id: string;
|
|
@@ -58,9 +99,13 @@ export interface GjcTeamTask {
|
|
|
58
99
|
assignee?: string;
|
|
59
100
|
owner?: string;
|
|
60
101
|
result?: string;
|
|
102
|
+
completion_evidence?: GjcTeamTaskCompletionEvidence;
|
|
61
103
|
error?: string;
|
|
62
104
|
blocked_by?: string[];
|
|
63
105
|
depends_on?: string[];
|
|
106
|
+
lane?: string;
|
|
107
|
+
required_role?: string;
|
|
108
|
+
allowed_roles?: string[];
|
|
64
109
|
version: number;
|
|
65
110
|
claim?: GjcTeamTaskClaim;
|
|
66
111
|
created_at: string;
|
|
@@ -121,6 +166,22 @@ export interface GjcTeamMonitorSnapshot {
|
|
|
121
166
|
integration_by_worker: Record<string, GjcTeamWorkerIntegrationState>;
|
|
122
167
|
updated_at: string;
|
|
123
168
|
}
|
|
169
|
+
export interface GjcTeamWorkerLifecycle {
|
|
170
|
+
worker: string;
|
|
171
|
+
lifecycle_state: GjcTeamWorkerLifecycleState;
|
|
172
|
+
worker_status_state: GjcWorkerStatusState;
|
|
173
|
+
pane_id?: string;
|
|
174
|
+
pid?: number;
|
|
175
|
+
started_at?: string;
|
|
176
|
+
updated_at: string;
|
|
177
|
+
stopped_at?: string;
|
|
178
|
+
stop_reason?: string;
|
|
179
|
+
shutdown_request_id?: string;
|
|
180
|
+
shutdown_requested_at?: string;
|
|
181
|
+
shutdown_acknowledged_at?: string;
|
|
182
|
+
shutdown_ack_status?: string;
|
|
183
|
+
shutdown_mode?: GjcTeamShutdownMode;
|
|
184
|
+
}
|
|
124
185
|
|
|
125
186
|
export type GjcTeamNotificationDeliveryState =
|
|
126
187
|
| "pending"
|
|
@@ -167,9 +228,13 @@ export interface GjcTeamSnapshot {
|
|
|
167
228
|
task_counts: Record<GjcTeamTaskStatus, number>;
|
|
168
229
|
workers: GjcTeamWorker[];
|
|
169
230
|
integration_by_worker?: Record<string, GjcTeamWorkerIntegrationState>;
|
|
231
|
+
worker_lifecycle_by_id: Record<string, GjcTeamWorkerLifecycle>;
|
|
170
232
|
notification_summary: GjcTeamNotificationSummary;
|
|
171
233
|
updated_at: string;
|
|
172
234
|
}
|
|
235
|
+
export interface GjcTeamSnapshotOptions {
|
|
236
|
+
reconcileNotifications?: boolean;
|
|
237
|
+
}
|
|
173
238
|
|
|
174
239
|
export interface GjcTeamStartOptions {
|
|
175
240
|
workerCount: number;
|
|
@@ -189,6 +254,23 @@ export interface GjcTeamApiClaimResult {
|
|
|
189
254
|
claim_token?: string;
|
|
190
255
|
reason?: string;
|
|
191
256
|
}
|
|
257
|
+
export type GjcTeamLivenessRecoveryReason =
|
|
258
|
+
| "claim_expired"
|
|
259
|
+
| "stale_heartbeat"
|
|
260
|
+
| "missing_pane"
|
|
261
|
+
| "worker_lifecycle_failed"
|
|
262
|
+
| "worker_lifecycle_stopped";
|
|
263
|
+
|
|
264
|
+
export interface GjcTeamRecoveredClaim {
|
|
265
|
+
task_id: string;
|
|
266
|
+
worker: string;
|
|
267
|
+
reasons: GjcTeamLivenessRecoveryReason[];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface GjcTeamLivenessRecoveryResult {
|
|
271
|
+
recovered_claims: GjcTeamRecoveredClaim[];
|
|
272
|
+
stale_workers: Record<string, GjcTeamLivenessRecoveryReason[]>;
|
|
273
|
+
}
|
|
192
274
|
|
|
193
275
|
export interface GjcTeamMailboxMessage {
|
|
194
276
|
message_id: string;
|
|
@@ -283,6 +365,19 @@ export interface GjcTeamEvent {
|
|
|
283
365
|
message?: string;
|
|
284
366
|
data?: Record<string, unknown>;
|
|
285
367
|
}
|
|
368
|
+
export interface GjcTeamTraceEvent {
|
|
369
|
+
schema_version: 1;
|
|
370
|
+
trace_id: string;
|
|
371
|
+
span_id: string;
|
|
372
|
+
source_event_id: string;
|
|
373
|
+
event_type: string;
|
|
374
|
+
ts: string;
|
|
375
|
+
worker?: string;
|
|
376
|
+
task_id?: string;
|
|
377
|
+
message?: string;
|
|
378
|
+
evidence_refs?: string[];
|
|
379
|
+
data?: Record<string, unknown>;
|
|
380
|
+
}
|
|
286
381
|
interface WorkerStatusFile {
|
|
287
382
|
state: GjcWorkerStatusState;
|
|
288
383
|
current_task_id?: string;
|
|
@@ -371,12 +466,15 @@ export const GJC_TEAM_API_OPERATIONS = [
|
|
|
371
466
|
"read-config",
|
|
372
467
|
"read-manifest",
|
|
373
468
|
"read-worker-status",
|
|
469
|
+
"update-worker-status",
|
|
374
470
|
"read-worker-heartbeat",
|
|
471
|
+
"recover-stale-claims",
|
|
375
472
|
"update-worker-heartbeat",
|
|
376
473
|
"write-worker-inbox",
|
|
377
474
|
"write-worker-identity",
|
|
378
475
|
"append-event",
|
|
379
476
|
"read-events",
|
|
477
|
+
"read-traces",
|
|
380
478
|
"await-event",
|
|
381
479
|
"write-shutdown-request",
|
|
382
480
|
"read-shutdown-ack",
|
|
@@ -392,9 +490,14 @@ function now(): string {
|
|
|
392
490
|
function isEnoent(error: unknown): error is FsError {
|
|
393
491
|
return typeof error === "object" && error !== null && "code" in error && (error as FsError).code === "ENOENT";
|
|
394
492
|
}
|
|
395
|
-
function
|
|
396
|
-
|
|
493
|
+
function stateWriterOptions(filePath: string, category: "state" | "ledger" | "report" | "prune", verb: string) {
|
|
494
|
+
const resolved = path.resolve(filePath);
|
|
495
|
+
const marker = `${path.sep}.gjc${path.sep}`;
|
|
496
|
+
const markerIndex = resolved.indexOf(marker);
|
|
497
|
+
const cwd = markerIndex >= 0 ? resolved.slice(0, markerIndex) : process.cwd();
|
|
498
|
+
return { cwd, audit: { category, verb, owner: "gjc-runtime" as const } };
|
|
397
499
|
}
|
|
500
|
+
|
|
398
501
|
function sanitizeName(value: string): string {
|
|
399
502
|
const sanitized = value
|
|
400
503
|
.toLowerCase()
|
|
@@ -432,9 +535,6 @@ function safePathSegment(kind: string, value: string): string {
|
|
|
432
535
|
function taskPath(dir: string, taskId: string): string {
|
|
433
536
|
return path.join(dir, "tasks", `${safePathSegment("task_id", taskId)}.json`);
|
|
434
537
|
}
|
|
435
|
-
function taskEvidencePath(dir: string, taskId: string): string {
|
|
436
|
-
return path.join(dir, "evidence", "tasks", `${safePathSegment("task_id", taskId)}.json`);
|
|
437
|
-
}
|
|
438
538
|
function mailboxPath(dir: string, worker: string): string {
|
|
439
539
|
return path.join(dir, "mailbox", `${safePathSegment("worker_id", worker)}.json`);
|
|
440
540
|
}
|
|
@@ -450,6 +550,17 @@ function notificationPath(dir: string, notificationId: string): string {
|
|
|
450
550
|
function workerDir(dir: string, worker: string): string {
|
|
451
551
|
return path.join(dir, "workers", safePathSegment("worker_id", worker));
|
|
452
552
|
}
|
|
553
|
+
function workerLifecyclePath(dir: string, worker: string): string {
|
|
554
|
+
return path.join(workerDir(dir, worker), "lifecycle.json");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function tracePath(dir: string): string {
|
|
558
|
+
return path.join(dir, "trace.jsonl");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function traceErrorPath(dir: string): string {
|
|
562
|
+
return path.join(dir, "trace-errors.jsonl");
|
|
563
|
+
}
|
|
453
564
|
function isSafeId(value: string): boolean {
|
|
454
565
|
return (
|
|
455
566
|
/^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(value) &&
|
|
@@ -469,6 +580,12 @@ function assertKnownWorker(config: GjcTeamConfig, worker: string, allowLeader =
|
|
|
469
580
|
if (allowLeader && isLeaderRecipient(worker)) return;
|
|
470
581
|
if (!config.workers.some(candidate => candidate.id === worker)) throw new Error(`unknown_worker:${worker}`);
|
|
471
582
|
}
|
|
583
|
+
function findKnownWorker(config: GjcTeamConfig, worker: string): GjcTeamWorker {
|
|
584
|
+
assertKnownWorker(config, worker);
|
|
585
|
+
const found = config.workers.find(candidate => candidate.id === worker);
|
|
586
|
+
if (!found) throw new Error(`unknown_worker:${worker}`);
|
|
587
|
+
return found;
|
|
588
|
+
}
|
|
472
589
|
function assertKnownParticipant(config: GjcTeamConfig, worker: string): void {
|
|
473
590
|
assertKnownWorker(config, worker, true);
|
|
474
591
|
}
|
|
@@ -503,33 +620,171 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
|
503
620
|
throw error;
|
|
504
621
|
}
|
|
505
622
|
}
|
|
623
|
+
function stateCategoryForJsonPath(filePath: string): "state" | "ledger" {
|
|
624
|
+
return filePath.endsWith(".jsonl") || filePath.includes(`${path.sep}telemetry${path.sep}`) ? "ledger" : "state";
|
|
625
|
+
}
|
|
626
|
+
|
|
506
627
|
async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
|
|
507
|
-
await
|
|
508
|
-
const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
509
|
-
await Bun.write(tmpPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
510
|
-
await fs.rename(tmpPath, filePath);
|
|
628
|
+
await writeJsonAtomic(filePath, value, stateWriterOptions(filePath, stateCategoryForJsonPath(filePath), "write"));
|
|
511
629
|
}
|
|
512
630
|
async function writeJsonFileNoClobber(filePath: string, value: unknown): Promise<boolean> {
|
|
513
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
514
|
-
let handle: fs.FileHandle | undefined;
|
|
515
631
|
try {
|
|
516
|
-
|
|
517
|
-
|
|
632
|
+
await createJsonNoClobber(
|
|
633
|
+
filePath,
|
|
634
|
+
value,
|
|
635
|
+
stateWriterOptions(filePath, stateCategoryForJsonPath(filePath), "create"),
|
|
636
|
+
);
|
|
518
637
|
return true;
|
|
519
638
|
} catch (error) {
|
|
520
|
-
if (
|
|
639
|
+
if (error instanceof AlreadyExistsError) return false;
|
|
521
640
|
throw error;
|
|
522
|
-
} finally {
|
|
523
|
-
await handle?.close();
|
|
524
641
|
}
|
|
525
642
|
}
|
|
526
643
|
async function appendJsonl(filePath: string, value: unknown): Promise<void> {
|
|
527
|
-
await
|
|
528
|
-
|
|
644
|
+
await appendJsonlAudited(filePath, value, stateWriterOptions(filePath, "ledger", "append"));
|
|
645
|
+
}
|
|
646
|
+
function traceIdForTeam(dir: string): string {
|
|
647
|
+
return `trace-${stableHash(path.basename(dir))}`;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function evidenceRefsForEvent(event: GjcTeamEvent): string[] | undefined {
|
|
651
|
+
const refs: string[] = [];
|
|
652
|
+
if (event.task_id && event.type === "task_transitioned" && event.data && "completion_evidence" in event.data)
|
|
653
|
+
refs.push(`task:${event.task_id}:completion_evidence`);
|
|
654
|
+
if (event.task_id && event.type === "task_claim_recovered") refs.push(`task:${event.task_id}:claim_recovery`);
|
|
655
|
+
if (event.worker && event.type.startsWith("worker_")) refs.push(`worker:${event.worker}`);
|
|
656
|
+
return refs.length > 0 ? refs : undefined;
|
|
657
|
+
}
|
|
658
|
+
function pickString(value: unknown): string | undefined {
|
|
659
|
+
return typeof value === "string" ? value : undefined;
|
|
660
|
+
}
|
|
661
|
+
function pickNumber(value: unknown): number | undefined {
|
|
662
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
663
|
+
}
|
|
664
|
+
function pickBoolean(value: unknown): boolean | undefined {
|
|
665
|
+
return typeof value === "boolean" ? value : undefined;
|
|
666
|
+
}
|
|
667
|
+
function pickStringArray(value: unknown): string[] | undefined {
|
|
668
|
+
return Array.isArray(value) && value.every(item => typeof item === "string") ? value : undefined;
|
|
669
|
+
}
|
|
670
|
+
function setIfDefined(record: Record<string, unknown>, key: string, value: unknown): void {
|
|
671
|
+
if (value !== undefined) record[key] = value;
|
|
672
|
+
}
|
|
673
|
+
function messageBodyTraceProjection(body: string | undefined): Record<string, unknown> {
|
|
674
|
+
if (body === undefined) return {};
|
|
675
|
+
return {
|
|
676
|
+
body_byte_length: Buffer.byteLength(body, "utf8"),
|
|
677
|
+
body_sha256: createHash("sha256").update(body).digest("hex"),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function traceDataForEvent(event: GjcTeamEvent): Record<string, unknown> | undefined {
|
|
681
|
+
const source = event.data ?? {};
|
|
682
|
+
const data: Record<string, unknown> = {};
|
|
683
|
+
switch (event.type) {
|
|
684
|
+
case "message_sent": {
|
|
685
|
+
setIfDefined(data, "to_worker", pickString(source.to_worker));
|
|
686
|
+
setIfDefined(data, "message_id", pickString(source.message_id));
|
|
687
|
+
Object.assign(data, messageBodyTraceProjection(pickString(event.message)));
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
case "message_acknowledged":
|
|
691
|
+
case "message_notified": {
|
|
692
|
+
setIfDefined(data, "message_id", pickString(event.message));
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
case "team_started": {
|
|
696
|
+
setIfDefined(data, "worker_count", pickNumber(source.worker_count));
|
|
697
|
+
setIfDefined(data, "agent_type", pickString(source.agent_type));
|
|
698
|
+
setIfDefined(data, "workspace_mode", pickString(source.workspace_mode));
|
|
699
|
+
setIfDefined(data, "dry_run", pickBoolean(source.dry_run));
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case "task_claim_recovered": {
|
|
703
|
+
setIfDefined(data, "reasons", pickStringArray(source.reasons));
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
case "task_transitioned": {
|
|
707
|
+
setIfDefined(data, "status", pickString(source.status));
|
|
708
|
+
const evidence = source.completion_evidence;
|
|
709
|
+
if (typeof evidence === "object" && evidence !== null) {
|
|
710
|
+
const evidenceRecord = evidence as Record<string, unknown>;
|
|
711
|
+
data.completion_evidence = {
|
|
712
|
+
recorded_by: pickString(evidenceRecord.recorded_by),
|
|
713
|
+
item_count: pickNumber(evidenceRecord.item_count),
|
|
714
|
+
verified_item_count: pickNumber(evidenceRecord.verified_item_count),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
case "worker_integration_attempt_requested": {
|
|
720
|
+
setIfDefined(data, "worker_name", pickString(source.worker_name));
|
|
721
|
+
setIfDefined(data, "worker_head", pickString(source.worker_head));
|
|
722
|
+
setIfDefined(data, "status", pickString(source.status));
|
|
723
|
+
if (Array.isArray(source.files)) data.file_count = source.files.length;
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
case "worker_lifecycle_nudge": {
|
|
727
|
+
setIfDefined(data, "condition", pickString(source.condition));
|
|
728
|
+
setIfDefined(data, "severity", pickString(source.severity));
|
|
729
|
+
setIfDefined(data, "fingerprint", pickString(source.fingerprint));
|
|
730
|
+
setIfDefined(data, "auto_action_taken", pickBoolean(source.auto_action_taken));
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
case "team_shutdown": {
|
|
734
|
+
setIfDefined(data, "phase", pickString(source.phase));
|
|
735
|
+
setIfDefined(data, "shutdown_request_id", pickString(source.shutdown_request_id));
|
|
736
|
+
setIfDefined(data, "graceful_shutdown_complete", pickBoolean(source.graceful_shutdown_complete));
|
|
737
|
+
if (Array.isArray(source.evidence_failures)) data.evidence_failure_count = source.evidence_failures.length;
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
case "worker_status_updated": {
|
|
741
|
+
setIfDefined(data, "status", pickString(source.status));
|
|
742
|
+
setIfDefined(data, "current_task_id", pickString(source.current_task_id));
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
case "worker_shutdown_requested": {
|
|
746
|
+
setIfDefined(data, "requested_by", pickString(source.requested_by));
|
|
747
|
+
setIfDefined(data, "request_id", pickString(source.request_id));
|
|
748
|
+
setIfDefined(data, "mode", pickString(source.mode));
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return Object.keys(data).length > 0 ? data : undefined;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function appendTraceForEvent(dir: string, event: GjcTeamEvent): Promise<void> {
|
|
756
|
+
const evidenceRefs = evidenceRefsForEvent(event);
|
|
757
|
+
const traceData = traceDataForEvent(event);
|
|
758
|
+
const trace: GjcTeamTraceEvent = {
|
|
759
|
+
schema_version: 1,
|
|
760
|
+
trace_id: traceIdForTeam(dir),
|
|
761
|
+
span_id: `span-${stableHash(event.event_id)}`,
|
|
762
|
+
source_event_id: event.event_id,
|
|
763
|
+
event_type: event.type,
|
|
764
|
+
ts: event.ts,
|
|
765
|
+
...(event.worker ? { worker: event.worker } : {}),
|
|
766
|
+
...(event.task_id ? { task_id: event.task_id } : {}),
|
|
767
|
+
...(traceData ? { data: traceData } : {}),
|
|
768
|
+
...(evidenceRefs ? { evidence_refs: evidenceRefs } : {}),
|
|
769
|
+
};
|
|
770
|
+
try {
|
|
771
|
+
await appendJsonl(tracePath(dir), trace);
|
|
772
|
+
} catch (error) {
|
|
773
|
+
try {
|
|
774
|
+
await appendJsonl(traceErrorPath(dir), {
|
|
775
|
+
ts: now(),
|
|
776
|
+
source_event_id: event.event_id,
|
|
777
|
+
error: error instanceof Error ? error.message : String(error),
|
|
778
|
+
});
|
|
779
|
+
} catch {
|
|
780
|
+
// Trace append failure must not break legacy events.jsonl compatibility.
|
|
781
|
+
}
|
|
782
|
+
}
|
|
529
783
|
}
|
|
530
784
|
async function appendEvent(dir: string, event: Omit<GjcTeamEvent, "ts" | "event_id">): Promise<GjcTeamEvent> {
|
|
531
785
|
const full = { event_id: `evt-${Date.now()}-${Math.random().toString(16).slice(2)}`, ts: now(), ...event };
|
|
532
786
|
await appendJsonl(path.join(dir, "events.jsonl"), full);
|
|
787
|
+
await appendTraceForEvent(dir, full);
|
|
533
788
|
return full;
|
|
534
789
|
}
|
|
535
790
|
async function appendTelemetry(
|
|
@@ -562,6 +817,325 @@ async function readPhase(dir: string): Promise<GjcTeamPhase> {
|
|
|
562
817
|
async function writePhase(dir: string, phase: GjcTeamPhase): Promise<void> {
|
|
563
818
|
await writeJsonFile(path.join(dir, "phase.json"), { current_phase: phase, updated_at: now() });
|
|
564
819
|
}
|
|
820
|
+
function isGjcWorkerStatusState(value: string): value is GjcWorkerStatusState {
|
|
821
|
+
return ["idle", "working", "blocked", "done", "failed", "draining", "unknown"].includes(value);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function parseGjcWorkerStatusState(value: unknown): GjcWorkerStatusState {
|
|
825
|
+
return typeof value === "string" && isGjcWorkerStatusState(value) ? value : "unknown";
|
|
826
|
+
}
|
|
827
|
+
function parseRequiredGjcWorkerStatusState(value: unknown): GjcWorkerStatusState {
|
|
828
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
829
|
+
if (isGjcWorkerStatusState(raw)) return raw;
|
|
830
|
+
throw new Error(`invalid_worker_status:${raw}`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function lifecycleStateForWorkerStatus(status: GjcWorkerStatusState): GjcTeamWorkerLifecycleState {
|
|
834
|
+
switch (status) {
|
|
835
|
+
case "working":
|
|
836
|
+
return "working";
|
|
837
|
+
case "draining":
|
|
838
|
+
return "draining";
|
|
839
|
+
case "failed":
|
|
840
|
+
return "failed";
|
|
841
|
+
case "unknown":
|
|
842
|
+
return "unknown";
|
|
843
|
+
case "idle":
|
|
844
|
+
case "blocked":
|
|
845
|
+
case "done":
|
|
846
|
+
return "ready";
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function parseGjcTeamShutdownMode(value: unknown): GjcTeamShutdownMode {
|
|
851
|
+
const raw = typeof value === "string" ? value.trim() : "graceful";
|
|
852
|
+
if (raw === "graceful" || raw === "force" || raw === "abort") return raw;
|
|
853
|
+
throw new Error(`invalid_shutdown_mode:${raw}`);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function isGjcTeamWorkerLifecycleState(value: string): value is GjcTeamWorkerLifecycleState {
|
|
857
|
+
return ["starting", "ready", "working", "draining", "stopped", "failed", "unknown"].includes(value);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function parseGjcTeamWorkerLifecycleState(value: unknown): GjcTeamWorkerLifecycleState {
|
|
861
|
+
return typeof value === "string" && isGjcTeamWorkerLifecycleState(value) ? value : "unknown";
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async function readWorkerStatusFile(dir: string, worker: string): Promise<WorkerStatusFile> {
|
|
865
|
+
return (
|
|
866
|
+
(await readJsonFile<WorkerStatusFile>(path.join(workerDir(dir, worker), "status.json"))) ?? {
|
|
867
|
+
state: "unknown",
|
|
868
|
+
updated_at: now(),
|
|
869
|
+
}
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async function readWorkerLifecycleRecord(dir: string, worker: GjcTeamWorker): Promise<GjcTeamWorkerLifecycle> {
|
|
874
|
+
const workerStatus = await readWorkerStatusFile(dir, worker.id);
|
|
875
|
+
const heartbeat = await readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker.id), "heartbeat.json"));
|
|
876
|
+
const rawLifecycle = await readJsonFile<Partial<GjcTeamWorkerLifecycle>>(workerLifecyclePath(dir, worker.id));
|
|
877
|
+
const shutdownAck = await readJsonFile<Record<string, unknown>>(
|
|
878
|
+
path.join(workerDir(dir, worker.id), "shutdown-ack.json"),
|
|
879
|
+
);
|
|
880
|
+
const lifecycle: GjcTeamWorkerLifecycle = {
|
|
881
|
+
worker: worker.id,
|
|
882
|
+
lifecycle_state: parseGjcTeamWorkerLifecycleState(rawLifecycle?.lifecycle_state),
|
|
883
|
+
worker_status_state: parseGjcWorkerStatusState(workerStatus.state),
|
|
884
|
+
pane_id: worker.pane_id ?? rawLifecycle?.pane_id,
|
|
885
|
+
updated_at: rawLifecycle?.updated_at ?? workerStatus.updated_at ?? now(),
|
|
886
|
+
};
|
|
887
|
+
if (typeof rawLifecycle?.pid === "number") lifecycle.pid = rawLifecycle.pid;
|
|
888
|
+
else if (typeof heartbeat?.pid === "number") lifecycle.pid = heartbeat.pid;
|
|
889
|
+
if (rawLifecycle?.started_at) lifecycle.started_at = rawLifecycle.started_at;
|
|
890
|
+
if (rawLifecycle?.stopped_at) lifecycle.stopped_at = rawLifecycle.stopped_at;
|
|
891
|
+
if (rawLifecycle?.stop_reason) lifecycle.stop_reason = rawLifecycle.stop_reason;
|
|
892
|
+
if (rawLifecycle?.shutdown_request_id) lifecycle.shutdown_request_id = rawLifecycle.shutdown_request_id;
|
|
893
|
+
if (rawLifecycle?.shutdown_requested_at) lifecycle.shutdown_requested_at = rawLifecycle.shutdown_requested_at;
|
|
894
|
+
if (
|
|
895
|
+
rawLifecycle?.shutdown_mode === "graceful" ||
|
|
896
|
+
rawLifecycle?.shutdown_mode === "force" ||
|
|
897
|
+
rawLifecycle?.shutdown_mode === "abort"
|
|
898
|
+
)
|
|
899
|
+
lifecycle.shutdown_mode = rawLifecycle.shutdown_mode;
|
|
900
|
+
if (typeof shutdownAck?.acknowledged_at === "string")
|
|
901
|
+
lifecycle.shutdown_acknowledged_at = shutdownAck.acknowledged_at;
|
|
902
|
+
if (typeof shutdownAck?.status === "string") lifecycle.shutdown_ack_status = shutdownAck.status;
|
|
903
|
+
return lifecycle;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function readWorkerLifecycleById(
|
|
907
|
+
dir: string,
|
|
908
|
+
config: GjcTeamConfig,
|
|
909
|
+
): Promise<Record<string, GjcTeamWorkerLifecycle>> {
|
|
910
|
+
const entries = await Promise.all(config.workers.map(worker => readWorkerLifecycleRecord(dir, worker)));
|
|
911
|
+
return Object.fromEntries(entries.map(entry => [entry.worker, entry]));
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
async function writeWorkerLifecycleRecord(
|
|
915
|
+
dir: string,
|
|
916
|
+
worker: GjcTeamWorker,
|
|
917
|
+
lifecycleState: GjcTeamWorkerLifecycleState,
|
|
918
|
+
updates: Partial<GjcTeamWorkerLifecycle> = {},
|
|
919
|
+
): Promise<GjcTeamWorkerLifecycle> {
|
|
920
|
+
const current = await readWorkerLifecycleRecord(dir, worker);
|
|
921
|
+
const next: GjcTeamWorkerLifecycle = {
|
|
922
|
+
...current,
|
|
923
|
+
...updates,
|
|
924
|
+
worker: worker.id,
|
|
925
|
+
lifecycle_state: lifecycleState,
|
|
926
|
+
worker_status_state: current.worker_status_state,
|
|
927
|
+
pane_id: updates.pane_id ?? worker.pane_id ?? current.pane_id,
|
|
928
|
+
updated_at: now(),
|
|
929
|
+
};
|
|
930
|
+
await writeJsonFile(workerLifecyclePath(dir, worker.id), next);
|
|
931
|
+
return next;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
async function writeWorkerLifecycleForConfig(
|
|
935
|
+
dir: string,
|
|
936
|
+
config: GjcTeamConfig,
|
|
937
|
+
lifecycleState: GjcTeamWorkerLifecycleState,
|
|
938
|
+
updatesFor: (worker: GjcTeamWorker) => Partial<GjcTeamWorkerLifecycle> = () => ({}),
|
|
939
|
+
): Promise<Record<string, GjcTeamWorkerLifecycle>> {
|
|
940
|
+
const entries = await Promise.all(
|
|
941
|
+
config.workers.map(worker => writeWorkerLifecycleRecord(dir, worker, lifecycleState, updatesFor(worker))),
|
|
942
|
+
);
|
|
943
|
+
return Object.fromEntries(entries.map(entry => [entry.worker, entry]));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function teamModeStatePath(): string {
|
|
947
|
+
return path.join(".gjc", "state", "team-state.json");
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
export async function persistGjcTeamModeStateSummary(snapshot: GjcTeamSnapshot, cwd = process.cwd()): Promise<void> {
|
|
951
|
+
const active = snapshot.phase !== "complete" && snapshot.phase !== "cancelled";
|
|
952
|
+
const updatedAt = now();
|
|
953
|
+
await writeJsonAtomic(
|
|
954
|
+
teamModeStatePath(),
|
|
955
|
+
{
|
|
956
|
+
skill: "team",
|
|
957
|
+
version: 1,
|
|
958
|
+
active,
|
|
959
|
+
current_phase: snapshot.phase,
|
|
960
|
+
team_name: snapshot.team_name,
|
|
961
|
+
task_counts: snapshot.task_counts,
|
|
962
|
+
updated_at: updatedAt,
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
cwd,
|
|
966
|
+
receipt: {
|
|
967
|
+
cwd,
|
|
968
|
+
skill: "team",
|
|
969
|
+
owner: "gjc-runtime",
|
|
970
|
+
command: "gjc team sync-team-summary",
|
|
971
|
+
nowIso: updatedAt,
|
|
972
|
+
},
|
|
973
|
+
audit: { category: "state", verb: "sync-team-summary", owner: "gjc-runtime", skill: "team" },
|
|
974
|
+
},
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function appendLivenessRecoveryReason(
|
|
979
|
+
reasons: GjcTeamLivenessRecoveryReason[],
|
|
980
|
+
reason: GjcTeamLivenessRecoveryReason,
|
|
981
|
+
): void {
|
|
982
|
+
if (!reasons.includes(reason)) reasons.push(reason);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function isPastTimestamp(value: string | undefined): boolean {
|
|
986
|
+
if (!value) return false;
|
|
987
|
+
const timestamp = Date.parse(value);
|
|
988
|
+
return Number.isFinite(timestamp) && timestamp <= Date.now();
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function readClaimRecord(value: unknown): GjcTeamTaskClaim | undefined {
|
|
992
|
+
if (!isRecord(value)) return undefined;
|
|
993
|
+
const owner = typeof value.owner === "string" ? value.owner : "";
|
|
994
|
+
const token = typeof value.token === "string" ? value.token : "";
|
|
995
|
+
const leasedUntil = typeof value.leased_until === "string" ? value.leased_until : "";
|
|
996
|
+
if (!owner || !token || !leasedUntil) return undefined;
|
|
997
|
+
return { owner, token, leased_until: leasedUntil };
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function isWorkerHeartbeatStale(
|
|
1001
|
+
worker: GjcTeamWorker,
|
|
1002
|
+
heartbeat: WorkerHeartbeatFile | null,
|
|
1003
|
+
env: NodeJS.ProcessEnv,
|
|
1004
|
+
): boolean {
|
|
1005
|
+
const thresholdMs = parseDurationEnv(env, "GJC_TEAM_HEARTBEAT_STALE_MS", 120_000);
|
|
1006
|
+
if (thresholdMs <= 0) return false;
|
|
1007
|
+
const heartbeatAt = Date.parse(heartbeat?.last_turn_at ?? worker.last_heartbeat);
|
|
1008
|
+
return Number.isFinite(heartbeatAt) && Date.now() - heartbeatAt >= thresholdMs;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async function detectGjcTeamWorkerLivenessReasons(
|
|
1012
|
+
dir: string,
|
|
1013
|
+
config: GjcTeamConfig,
|
|
1014
|
+
worker: GjcTeamWorker,
|
|
1015
|
+
env: NodeJS.ProcessEnv,
|
|
1016
|
+
): Promise<GjcTeamLivenessRecoveryReason[]> {
|
|
1017
|
+
const reasons: GjcTeamLivenessRecoveryReason[] = [];
|
|
1018
|
+
const lifecycle = await readWorkerLifecycleRecord(dir, worker);
|
|
1019
|
+
const heartbeat = await readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker.id), "heartbeat.json"));
|
|
1020
|
+
if (lifecycle.lifecycle_state === "failed") appendLivenessRecoveryReason(reasons, "worker_lifecycle_failed");
|
|
1021
|
+
if (lifecycle.lifecycle_state === "stopped") appendLivenessRecoveryReason(reasons, "worker_lifecycle_stopped");
|
|
1022
|
+
if (isWorkerHeartbeatStale(worker, heartbeat, env)) appendLivenessRecoveryReason(reasons, "stale_heartbeat");
|
|
1023
|
+
if (!config.dry_run && (!worker.pane_id?.startsWith("%") || !paneBelongsToTeamTarget(config, worker.pane_id)))
|
|
1024
|
+
appendLivenessRecoveryReason(reasons, "missing_pane");
|
|
1025
|
+
return reasons;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
async function reconcileGjcTeamStaleClaims(
|
|
1029
|
+
teamName: string,
|
|
1030
|
+
dir: string,
|
|
1031
|
+
config: GjcTeamConfig,
|
|
1032
|
+
env: NodeJS.ProcessEnv,
|
|
1033
|
+
): Promise<GjcTeamLivenessRecoveryResult> {
|
|
1034
|
+
const staleWorkers: Record<string, GjcTeamLivenessRecoveryReason[]> = {};
|
|
1035
|
+
for (const worker of config.workers) {
|
|
1036
|
+
const reasons = await detectGjcTeamWorkerLivenessReasons(dir, config, worker, env);
|
|
1037
|
+
if (reasons.length === 0) continue;
|
|
1038
|
+
staleWorkers[worker.id] = reasons;
|
|
1039
|
+
if (reasons.includes("missing_pane") && reasons.includes("worker_lifecycle_stopped") === false) {
|
|
1040
|
+
await writeWorkerLifecycleRecord(dir, worker, "failed", { stop_reason: "pane_missing" });
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const recoveredClaims: GjcTeamRecoveredClaim[] = [];
|
|
1045
|
+
for (const task of await readTasks(dir)) {
|
|
1046
|
+
if (task.status === "completed" || task.status === "failed") continue;
|
|
1047
|
+
const claimPath = path.join(dir, "claims", `${task.id}.json`);
|
|
1048
|
+
const diskClaim = readClaimRecord(await readJsonFile<unknown>(claimPath));
|
|
1049
|
+
const claim = task.claim ?? diskClaim;
|
|
1050
|
+
if (!claim) continue;
|
|
1051
|
+
|
|
1052
|
+
const reasons = [...(staleWorkers[claim.owner] ?? [])];
|
|
1053
|
+
if (isPastTimestamp(claim.leased_until)) appendLivenessRecoveryReason(reasons, "claim_expired");
|
|
1054
|
+
if (reasons.length === 0) continue;
|
|
1055
|
+
|
|
1056
|
+
await fs.rm(claimPath, { force: true });
|
|
1057
|
+
recoveredClaims.push({ task_id: task.id, worker: claim.owner, reasons });
|
|
1058
|
+
if (task.status !== "in_progress") {
|
|
1059
|
+
await appendEvent(dir, {
|
|
1060
|
+
type: "task_claim_recovered",
|
|
1061
|
+
task_id: task.id,
|
|
1062
|
+
worker: claim.owner,
|
|
1063
|
+
message: "Removed stale task claim file",
|
|
1064
|
+
data: { reasons },
|
|
1065
|
+
});
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const recoveredTask = normalizeTask({
|
|
1070
|
+
...task,
|
|
1071
|
+
status: "pending",
|
|
1072
|
+
assignee: undefined,
|
|
1073
|
+
claim: undefined,
|
|
1074
|
+
version: task.version + 1,
|
|
1075
|
+
updated_at: now(),
|
|
1076
|
+
});
|
|
1077
|
+
await writeTask(dir, recoveredTask);
|
|
1078
|
+
await appendEvent(dir, {
|
|
1079
|
+
type: "task_claim_recovered",
|
|
1080
|
+
task_id: task.id,
|
|
1081
|
+
worker: claim.owner,
|
|
1082
|
+
message: "Recovered task from stale worker claim",
|
|
1083
|
+
data: { reasons },
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (recoveredClaims.length > 0)
|
|
1088
|
+
await appendTelemetry(dir, {
|
|
1089
|
+
type: "team_liveness_recovery",
|
|
1090
|
+
message: `Recovered ${recoveredClaims.length} stale team task claim(s)`,
|
|
1091
|
+
data: { team_name: teamName, recovered_claims: recoveredClaims },
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
return { recovered_claims: recoveredClaims, stale_workers: staleWorkers };
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
export async function recoverGjcTeamStaleClaims(
|
|
1098
|
+
teamName: string,
|
|
1099
|
+
cwd = process.cwd(),
|
|
1100
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
1101
|
+
): Promise<GjcTeamLivenessRecoveryResult> {
|
|
1102
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
1103
|
+
const config = await readConfig(dir);
|
|
1104
|
+
return reconcileGjcTeamStaleClaims(teamName, dir, config, env);
|
|
1105
|
+
}
|
|
1106
|
+
function normalizeOptionalTaskString(value: unknown): string | undefined {
|
|
1107
|
+
if (typeof value !== "string") return undefined;
|
|
1108
|
+
const trimmed = value.trim();
|
|
1109
|
+
return trimmed || undefined;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function normalizeOptionalTaskStringArray(value: unknown): string[] | undefined {
|
|
1113
|
+
if (!Array.isArray(value)) return undefined;
|
|
1114
|
+
const items = Array.from(
|
|
1115
|
+
new Set(value.map(item => (typeof item === "string" ? item.trim() : "")).filter(item => item.length > 0)),
|
|
1116
|
+
).sort();
|
|
1117
|
+
return items.length > 0 ? items : undefined;
|
|
1118
|
+
}
|
|
1119
|
+
type GjcTeamTaskMetadataInput = Partial<
|
|
1120
|
+
Pick<GjcTeamTask, "owner" | "lane" | "required_role" | "allowed_roles" | "depends_on" | "blocked_by">
|
|
1121
|
+
>;
|
|
1122
|
+
|
|
1123
|
+
function taskMetadataFromInput(input: Record<string, unknown>, includeOwner = false): GjcTeamTaskMetadataInput {
|
|
1124
|
+
const metadata: GjcTeamTaskMetadataInput = {};
|
|
1125
|
+
const owner = normalizeOptionalTaskString(input.owner);
|
|
1126
|
+
const lane = normalizeOptionalTaskString(input.lane);
|
|
1127
|
+
const requiredRole = normalizeOptionalTaskString(input.required_role ?? input.requiredRole);
|
|
1128
|
+
const allowedRoles = normalizeOptionalTaskStringArray(input.allowed_roles ?? input.allowedRoles);
|
|
1129
|
+
const dependsOn = normalizeOptionalTaskStringArray(input.depends_on ?? input.dependsOn);
|
|
1130
|
+
const blockedBy = normalizeOptionalTaskStringArray(input.blocked_by ?? input.blockedBy);
|
|
1131
|
+
if (includeOwner && owner) metadata.owner = owner;
|
|
1132
|
+
if (lane) metadata.lane = lane;
|
|
1133
|
+
if (requiredRole) metadata.required_role = requiredRole;
|
|
1134
|
+
if (allowedRoles) metadata.allowed_roles = allowedRoles;
|
|
1135
|
+
if (dependsOn) metadata.depends_on = dependsOn;
|
|
1136
|
+
if (blockedBy) metadata.blocked_by = blockedBy;
|
|
1137
|
+
return metadata;
|
|
1138
|
+
}
|
|
565
1139
|
|
|
566
1140
|
function normalizeTask(raw: GjcTeamTask): GjcTeamTask {
|
|
567
1141
|
const status = raw.status === ("complete" as GjcTeamTaskStatus) ? "completed" : raw.status;
|
|
@@ -573,6 +1147,9 @@ function normalizeTask(raw: GjcTeamTask): GjcTeamTask {
|
|
|
573
1147
|
title: raw.title ?? raw.subject,
|
|
574
1148
|
objective: raw.objective ?? raw.description,
|
|
575
1149
|
version: raw.version ?? 1,
|
|
1150
|
+
lane: normalizeOptionalTaskString(raw.lane),
|
|
1151
|
+
required_role: normalizeOptionalTaskString(raw.required_role),
|
|
1152
|
+
allowed_roles: normalizeOptionalTaskStringArray(raw.allowed_roles),
|
|
576
1153
|
};
|
|
577
1154
|
}
|
|
578
1155
|
|
|
@@ -616,13 +1193,189 @@ async function resolveGjcTeamSnapshotPhase(
|
|
|
616
1193
|
monitor: GjcTeamMonitorSnapshot | null,
|
|
617
1194
|
): Promise<GjcTeamPhase> {
|
|
618
1195
|
if (storedPhase !== "running") return storedPhase;
|
|
619
|
-
if (tasks.length === 0 || !tasks.every(
|
|
1196
|
+
if (tasks.length === 0 || !tasks.every(isGjcTeamTaskCompletionVerified)) return storedPhase;
|
|
620
1197
|
return (await hasPendingGjcTeamIntegration(dir, config, monitor)) ? "awaiting_integration" : storedPhase;
|
|
621
1198
|
}
|
|
622
1199
|
|
|
623
1200
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
624
1201
|
return typeof value === "object" && value != null;
|
|
625
1202
|
}
|
|
1203
|
+
const GJC_TEAM_COMPLETION_EVIDENCE_SUMMARY_MAX = 4_000;
|
|
1204
|
+
const GJC_TEAM_COMPLETION_EVIDENCE_OUTPUT_MAX = 8_000;
|
|
1205
|
+
const GJC_TEAM_COMMAND_EVIDENCE_STATUSES = new Set<GjcTeamTaskCompletionEvidenceStatus>([
|
|
1206
|
+
"passed",
|
|
1207
|
+
"failed",
|
|
1208
|
+
"not_run",
|
|
1209
|
+
]);
|
|
1210
|
+
const GJC_TEAM_VERIFICATION_EVIDENCE_STATUSES = new Set<GjcTeamTaskCompletionEvidenceStatus>(["verified", "rejected"]);
|
|
1211
|
+
|
|
1212
|
+
function completionEvidenceError(taskId: string, field: string): Error {
|
|
1213
|
+
return new Error(`invalid_completion_evidence:${taskId}:${field}`);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function trimRequiredCompletionEvidenceString(
|
|
1217
|
+
taskId: string,
|
|
1218
|
+
field: string,
|
|
1219
|
+
value: unknown,
|
|
1220
|
+
maxLength = GJC_TEAM_COMPLETION_EVIDENCE_SUMMARY_MAX,
|
|
1221
|
+
): string {
|
|
1222
|
+
if (typeof value !== "string") throw completionEvidenceError(taskId, field);
|
|
1223
|
+
const trimmed = value.trim();
|
|
1224
|
+
if (!trimmed || trimmed.length > maxLength) throw completionEvidenceError(taskId, field);
|
|
1225
|
+
return trimmed;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function trimOptionalCompletionEvidenceString(
|
|
1229
|
+
taskId: string,
|
|
1230
|
+
field: string,
|
|
1231
|
+
value: unknown,
|
|
1232
|
+
maxLength = GJC_TEAM_COMPLETION_EVIDENCE_OUTPUT_MAX,
|
|
1233
|
+
): string | undefined {
|
|
1234
|
+
if (value == null) return undefined;
|
|
1235
|
+
if (typeof value !== "string") throw completionEvidenceError(taskId, field);
|
|
1236
|
+
const trimmed = value.trim();
|
|
1237
|
+
if (!trimmed) return undefined;
|
|
1238
|
+
if (trimmed.length > maxLength) throw completionEvidenceError(taskId, field);
|
|
1239
|
+
return trimmed;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function normalizeGjcTeamCompletionEvidenceStatus(
|
|
1243
|
+
taskId: string,
|
|
1244
|
+
kind: GjcTeamTaskCompletionEvidenceKind,
|
|
1245
|
+
value: unknown,
|
|
1246
|
+
): GjcTeamTaskCompletionEvidenceStatus {
|
|
1247
|
+
const status = trimRequiredCompletionEvidenceString(taskId, "items.status", value);
|
|
1248
|
+
const allowed = kind === "command" ? GJC_TEAM_COMMAND_EVIDENCE_STATUSES : GJC_TEAM_VERIFICATION_EVIDENCE_STATUSES;
|
|
1249
|
+
if (!allowed.has(status as GjcTeamTaskCompletionEvidenceStatus))
|
|
1250
|
+
throw completionEvidenceError(taskId, "items.status");
|
|
1251
|
+
return status as GjcTeamTaskCompletionEvidenceStatus;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function normalizeGjcTeamCompletionEvidenceItem(taskId: string, value: unknown): GjcTeamTaskCompletionEvidenceItem {
|
|
1255
|
+
if (!isRecord(value) || Array.isArray(value)) throw completionEvidenceError(taskId, "items");
|
|
1256
|
+
const kind = trimRequiredCompletionEvidenceString(taskId, "items.kind", value.kind);
|
|
1257
|
+
if (kind !== "command" && kind !== "inspection" && kind !== "artifact")
|
|
1258
|
+
throw completionEvidenceError(taskId, "items.kind");
|
|
1259
|
+
const status = normalizeGjcTeamCompletionEvidenceStatus(taskId, kind, value.status);
|
|
1260
|
+
const item: GjcTeamTaskCompletionEvidenceItem = {
|
|
1261
|
+
kind,
|
|
1262
|
+
status,
|
|
1263
|
+
summary: trimRequiredCompletionEvidenceString(taskId, "items.summary", value.summary),
|
|
1264
|
+
};
|
|
1265
|
+
const command = trimOptionalCompletionEvidenceString(taskId, "items.command", value.command);
|
|
1266
|
+
const artifact = trimOptionalCompletionEvidenceString(taskId, "items.artifact", value.artifact);
|
|
1267
|
+
const location = trimOptionalCompletionEvidenceString(taskId, "items.location", value.location);
|
|
1268
|
+
const output = trimOptionalCompletionEvidenceString(taskId, "items.output", value.output);
|
|
1269
|
+
if (kind === "command" && !command) throw completionEvidenceError(taskId, "items.command");
|
|
1270
|
+
if (command) item.command = command;
|
|
1271
|
+
if (artifact) item.artifact = artifact;
|
|
1272
|
+
if (location) item.location = location;
|
|
1273
|
+
if (output) item.output = output;
|
|
1274
|
+
return item;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function normalizeGjcTeamCompletionEvidenceFiles(taskId: string, value: unknown): string[] | undefined {
|
|
1278
|
+
if (value == null) return undefined;
|
|
1279
|
+
if (!Array.isArray(value)) throw completionEvidenceError(taskId, "files");
|
|
1280
|
+
const files = new Set<string>();
|
|
1281
|
+
for (const entry of value) {
|
|
1282
|
+
if (typeof entry !== "string") throw completionEvidenceError(taskId, "files");
|
|
1283
|
+
const filePath = entry.trim().replace(/\\/g, "/");
|
|
1284
|
+
if (!filePath || filePath.includes("\0") || path.isAbsolute(filePath) || filePath.split("/").includes("..")) {
|
|
1285
|
+
throw completionEvidenceError(taskId, "files");
|
|
1286
|
+
}
|
|
1287
|
+
files.add(filePath);
|
|
1288
|
+
}
|
|
1289
|
+
return files.size > 0 ? [...files].sort() : undefined;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function isGjcTeamCompletionEvidenceItemVerified(item: GjcTeamTaskCompletionEvidenceItem): boolean {
|
|
1293
|
+
return (
|
|
1294
|
+
(item.kind === "command" && item.status === "passed") ||
|
|
1295
|
+
((item.kind === "inspection" || item.kind === "artifact") && item.status === "verified")
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function normalizeGjcTeamTaskCompletionEvidence(
|
|
1300
|
+
taskId: string,
|
|
1301
|
+
owner: string,
|
|
1302
|
+
input: unknown,
|
|
1303
|
+
recordedAt = now(),
|
|
1304
|
+
): GjcTeamTaskCompletionEvidence {
|
|
1305
|
+
if (!isRecord(input) || Array.isArray(input)) throw new Error(`completion_evidence_required:${taskId}`);
|
|
1306
|
+
const itemsValue = input.items;
|
|
1307
|
+
if (!Array.isArray(itemsValue) || itemsValue.length === 0) throw completionEvidenceError(taskId, "items");
|
|
1308
|
+
const items = itemsValue.map(item => normalizeGjcTeamCompletionEvidenceItem(taskId, item));
|
|
1309
|
+
if (!items.some(isGjcTeamCompletionEvidenceItemVerified))
|
|
1310
|
+
throw new Error(`completion_evidence_no_verified_item:${taskId}`);
|
|
1311
|
+
const evidence: GjcTeamTaskCompletionEvidence = {
|
|
1312
|
+
summary: trimRequiredCompletionEvidenceString(taskId, "summary", input.summary),
|
|
1313
|
+
items,
|
|
1314
|
+
recorded_by: owner,
|
|
1315
|
+
recorded_at: recordedAt,
|
|
1316
|
+
};
|
|
1317
|
+
const files = normalizeGjcTeamCompletionEvidenceFiles(taskId, input.files);
|
|
1318
|
+
const notes = trimOptionalCompletionEvidenceString(taskId, "notes", input.notes);
|
|
1319
|
+
if (files) evidence.files = files;
|
|
1320
|
+
if (notes) evidence.notes = notes;
|
|
1321
|
+
return evidence;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function getGjcTeamTaskCompletionEvidenceFailure(task: GjcTeamTask): string | null {
|
|
1325
|
+
if (task.status !== "completed") return `task_not_completed:${task.id}`;
|
|
1326
|
+
const evidence = task.completion_evidence;
|
|
1327
|
+
if (!isRecord(evidence) || Array.isArray(evidence)) return `completion_evidence_required:${task.id}`;
|
|
1328
|
+
if (typeof evidence.recorded_by !== "string" || evidence.recorded_by.trim().length === 0)
|
|
1329
|
+
return `invalid_completion_evidence:${task.id}:recorded_by`;
|
|
1330
|
+
if (typeof evidence.recorded_at !== "string" || evidence.recorded_at.trim().length === 0)
|
|
1331
|
+
return `invalid_completion_evidence:${task.id}:recorded_at`;
|
|
1332
|
+
try {
|
|
1333
|
+
normalizeGjcTeamTaskCompletionEvidence(task.id, evidence.recorded_by.trim(), evidence, evidence.recorded_at);
|
|
1334
|
+
return null;
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
return error instanceof Error ? error.message : `invalid_completion_evidence:${task.id}:unknown`;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function isGjcTeamTaskCompletionVerified(task: GjcTeamTask): boolean {
|
|
1341
|
+
return getGjcTeamTaskCompletionEvidenceFailure(task) == null;
|
|
1342
|
+
}
|
|
1343
|
+
function roleValuesForWorker(worker: GjcTeamWorker): Set<string> {
|
|
1344
|
+
return new Set([worker.role, worker.agent_type].map(value => value.trim()).filter(value => value.length > 0));
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function getGjcTeamTaskClaimEligibilityReason(
|
|
1348
|
+
task: GjcTeamTask,
|
|
1349
|
+
worker: GjcTeamWorker,
|
|
1350
|
+
tasks: GjcTeamTask[],
|
|
1351
|
+
): string | null {
|
|
1352
|
+
if (task.status !== "pending") return `task_not_pending:${task.id}`;
|
|
1353
|
+
if (task.owner && task.owner !== worker.id) return `task_owner_mismatch:${task.id}:${task.owner}`;
|
|
1354
|
+
if (task.assignee && task.assignee !== worker.id) return `task_assignee_mismatch:${task.id}:${task.assignee}`;
|
|
1355
|
+
|
|
1356
|
+
const workerRoles = roleValuesForWorker(worker);
|
|
1357
|
+
if (task.required_role && !workerRoles.has(task.required_role))
|
|
1358
|
+
return `task_role_mismatch:${task.id}:${task.required_role}`;
|
|
1359
|
+
if (task.allowed_roles?.length && !task.allowed_roles.some(role => workerRoles.has(role)))
|
|
1360
|
+
return `task_role_mismatch:${task.id}:${task.allowed_roles.join(",")}`;
|
|
1361
|
+
|
|
1362
|
+
if (task.blocked_by?.length) return `task_blocked:${task.id}:${task.blocked_by.join(",")}`;
|
|
1363
|
+
for (const dependencyId of task.depends_on ?? []) {
|
|
1364
|
+
const dependency = tasks.find(candidate => candidate.id === dependencyId);
|
|
1365
|
+
if (!dependency || !isGjcTeamTaskCompletionVerified(dependency))
|
|
1366
|
+
return `task_dependency_incomplete:${task.id}:${dependencyId}`;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
async function getActiveClaimReason(dir: string, task: GjcTeamTask): Promise<string | null> {
|
|
1373
|
+
const claimPath = path.join(dir, "claims", `${task.id}.json`);
|
|
1374
|
+
const diskClaim = readClaimRecord(await readJsonFile<unknown>(claimPath));
|
|
1375
|
+
const claim = task.claim ?? diskClaim;
|
|
1376
|
+
if (!claim || isPastTimestamp(claim.leased_until)) return null;
|
|
1377
|
+
return `task_already_claimed:${task.id}`;
|
|
1378
|
+
}
|
|
626
1379
|
function isGjcTeamTaskRecord(value: unknown): value is GjcTeamTask {
|
|
627
1380
|
return (
|
|
628
1381
|
isRecord(value) &&
|
|
@@ -820,17 +1573,35 @@ async function ensureWorkerWorktree(
|
|
|
820
1573
|
export function resolveGjcTmuxCommand(env: NodeJS.ProcessEnv = process.env): string {
|
|
821
1574
|
return env.GJC_TEAM_TMUX_COMMAND?.trim() || "tmux";
|
|
822
1575
|
}
|
|
1576
|
+
function buildTeamTmuxLeaderRequirementMessage(detail?: string): string {
|
|
1577
|
+
const suffix = detail?.trim() ? `:${detail.trim()}` : "";
|
|
1578
|
+
return `gjc_team_requires_tmux_leader: run \`gjc --tmux\` first, then run \`gjc team ...\` inside that tmux-backed leader session, or use \`gjc team --dry-run\` for state-only smoke tests${suffix}`;
|
|
1579
|
+
}
|
|
1580
|
+
function readGjcTmuxProfileValue(tmuxCommand: string, sessionName: string): string {
|
|
1581
|
+
const result = Bun.spawnSync(
|
|
1582
|
+
[tmuxCommand, "show-options", "-qv", "-t", `=${sessionName}`, GJC_TMUX_PROFILE_OPTION],
|
|
1583
|
+
{
|
|
1584
|
+
stdout: "pipe",
|
|
1585
|
+
stderr: "pipe",
|
|
1586
|
+
},
|
|
1587
|
+
);
|
|
1588
|
+
if (result.exitCode !== 0) return "";
|
|
1589
|
+
return result.stdout.toString().trim();
|
|
1590
|
+
}
|
|
1591
|
+
|
|
823
1592
|
function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEnv): GjcTmuxLeaderContext {
|
|
824
1593
|
const paneTarget = env.TMUX_PANE?.trim();
|
|
825
1594
|
const args = paneTarget
|
|
826
1595
|
? ["display-message", "-p", "-t", paneTarget, "#S:#I #{pane_id}"]
|
|
827
1596
|
: ["display-message", "-p", "#S:#I #{pane_id}"];
|
|
828
1597
|
const result = Bun.spawnSync([tmuxCommand, ...args], { stdout: "pipe", stderr: "pipe" });
|
|
829
|
-
if (result.exitCode !== 0) throw new Error(result.stderr.toString()
|
|
1598
|
+
if (result.exitCode !== 0) throw new Error(buildTeamTmuxLeaderRequirementMessage(result.stderr.toString()));
|
|
830
1599
|
const [sessionAndWindow = "", leaderPaneId = ""] = result.stdout.toString().trim().split(/\s+/);
|
|
831
1600
|
const [sessionName = "", windowIndex = ""] = sessionAndWindow.split(":");
|
|
832
1601
|
if (!sessionName || !windowIndex || !leaderPaneId.startsWith("%"))
|
|
833
|
-
throw new Error(`invalid_tmux_context:${result.stdout.toString().trim()}`);
|
|
1602
|
+
throw new Error(buildTeamTmuxLeaderRequirementMessage(`invalid_tmux_context:${result.stdout.toString().trim()}`));
|
|
1603
|
+
if (readGjcTmuxProfileValue(tmuxCommand, sessionName) !== GJC_TMUX_PROFILE_VALUE)
|
|
1604
|
+
throw new Error(buildTeamTmuxLeaderRequirementMessage(`unmanaged_tmux_session:${sessionName}`));
|
|
834
1605
|
return { sessionName, windowIndex, leaderPaneId, target: `${sessionName}:${windowIndex}` };
|
|
835
1606
|
}
|
|
836
1607
|
export function resolveGjcWorkerCommand(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
|
|
@@ -852,7 +1623,7 @@ function buildWorkerCommand(config: GjcTeamConfig, worker: GjcTeamWorker): strin
|
|
|
852
1623
|
workspace,
|
|
853
1624
|
`Task: ${config.task}`,
|
|
854
1625
|
`Before claiming work, send startup ACK: gjc team api worker-startup-ack --input '{"team_name":"${config.team_name}","worker_id":"${worker.id}","protocol_version":"1"}' --json.`,
|
|
855
|
-
`Use gjc team api claim-task/transition-task-status with this worker id
|
|
1626
|
+
`Use gjc team api update-worker-status to report task-local activity, then claim-task/transition-task-status with this worker id; record completion_evidence (summary plus a passed command or verified inspection/artifact item) before completed, and do not mutate leader-owned goal state.`,
|
|
856
1627
|
].join("\n");
|
|
857
1628
|
const env = [
|
|
858
1629
|
`GJC_TEAM_WORKER=${shellQuote(`${config.team_name}/${worker.id}`)}`,
|
|
@@ -1032,9 +1803,18 @@ async function appendIntegrationReport(
|
|
|
1032
1803
|
entry: { worker: string; operation: "merge" | "cherry-pick" | "rebase"; files: string[]; detail: string },
|
|
1033
1804
|
): Promise<void> {
|
|
1034
1805
|
const line = `- [${now()}] ${entry.worker}: ${entry.operation}; files=${entry.files.join(",") || "unknown"}; ${entry.detail}\n`;
|
|
1035
|
-
await
|
|
1036
|
-
|
|
1037
|
-
|
|
1806
|
+
if (await pathExists(integrationReportPath(dir)))
|
|
1807
|
+
await appendText(
|
|
1808
|
+
integrationReportPath(dir),
|
|
1809
|
+
line,
|
|
1810
|
+
stateWriterOptions(integrationReportPath(dir), "report", "append"),
|
|
1811
|
+
);
|
|
1812
|
+
else
|
|
1813
|
+
await writeReport(
|
|
1814
|
+
integrationReportPath(dir),
|
|
1815
|
+
`# Integration Report\n\n${line}`,
|
|
1816
|
+
stateWriterOptions(integrationReportPath(dir), "report", "write"),
|
|
1817
|
+
);
|
|
1038
1818
|
}
|
|
1039
1819
|
async function appendCommitHygieneEntries(config: GjcTeamConfig, entries: GjcTeamCommitHygieneEntry[]): Promise<void> {
|
|
1040
1820
|
if (entries.length === 0) return;
|
|
@@ -1583,13 +2363,18 @@ async function integrateGjcWorkerCommits(
|
|
|
1583
2363
|
}
|
|
1584
2364
|
|
|
1585
2365
|
async function initializeStateDirs(dir: string, workers: GjcTeamWorker[]): Promise<void> {
|
|
1586
|
-
|
|
1587
|
-
|
|
2366
|
+
// Empty mailbox directories are runtime state, so they must exist before messages arrive.
|
|
2367
|
+
await fs.mkdir(path.join(dir, "mailbox"), { recursive: true });
|
|
1588
2368
|
for (const worker of workers) {
|
|
1589
|
-
await fs.mkdir(workerDir(dir, worker.id), { recursive: true });
|
|
1590
2369
|
await fs.mkdir(mailboxDirPath(dir, worker.id), { recursive: true });
|
|
1591
2370
|
await writeJsonFile(mailboxPath(dir, worker.id), { messages: [] });
|
|
1592
2371
|
await writeJsonFile(path.join(workerDir(dir, worker.id), "status.json"), { state: "idle", updated_at: now() });
|
|
2372
|
+
await writeJsonFile(workerLifecyclePath(dir, worker.id), {
|
|
2373
|
+
worker: worker.id,
|
|
2374
|
+
lifecycle_state: "starting",
|
|
2375
|
+
worker_status_state: "idle",
|
|
2376
|
+
updated_at: now(),
|
|
2377
|
+
} satisfies GjcTeamWorkerLifecycle);
|
|
1593
2378
|
await writeJsonFile(path.join(workerDir(dir, worker.id), "heartbeat.json"), {
|
|
1594
2379
|
pid: 0,
|
|
1595
2380
|
last_turn_at: now(),
|
|
@@ -1597,6 +2382,7 @@ async function initializeStateDirs(dir: string, workers: GjcTeamWorker[]): Promi
|
|
|
1597
2382
|
alive: true,
|
|
1598
2383
|
});
|
|
1599
2384
|
}
|
|
2385
|
+
// Empty leader mailbox directory is runtime state, so it must exist before messages arrive.
|
|
1600
2386
|
await fs.mkdir(mailboxDirPath(dir, "leader-fixed"), { recursive: true });
|
|
1601
2387
|
await writeJsonFile(mailboxPath(dir, "leader-fixed"), { messages: [] });
|
|
1602
2388
|
}
|
|
@@ -1714,6 +2500,10 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
1714
2500
|
updated_at: now(),
|
|
1715
2501
|
};
|
|
1716
2502
|
await writeJsonFile(path.join(dir, "config.json"), runningConfig);
|
|
2503
|
+
await writeWorkerLifecycleForConfig(dir, runningConfig, "starting", worker => ({
|
|
2504
|
+
pane_id: worker.pane_id,
|
|
2505
|
+
started_at: runningConfig.created_at,
|
|
2506
|
+
}));
|
|
1717
2507
|
await writePhase(dir, "running");
|
|
1718
2508
|
return readGjcTeamSnapshot(teamName, cwd, env);
|
|
1719
2509
|
}
|
|
@@ -1722,6 +2512,7 @@ export async function readGjcTeamSnapshot(
|
|
|
1722
2512
|
teamName: string,
|
|
1723
2513
|
cwd = process.cwd(),
|
|
1724
2514
|
env: NodeJS.ProcessEnv = process.env,
|
|
2515
|
+
options: GjcTeamSnapshotOptions = {},
|
|
1725
2516
|
): Promise<GjcTeamSnapshot> {
|
|
1726
2517
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
1727
2518
|
const config = await readConfig(dir);
|
|
@@ -1736,7 +2527,11 @@ export async function readGjcTeamSnapshot(
|
|
|
1736
2527
|
};
|
|
1737
2528
|
for (const task of tasks) taskCounts[task.status] += 1;
|
|
1738
2529
|
const monitor = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
|
|
1739
|
-
const
|
|
2530
|
+
const workerLifecycleById = await readWorkerLifecycleById(dir, config);
|
|
2531
|
+
const notificationSummary =
|
|
2532
|
+
options.reconcileNotifications === true
|
|
2533
|
+
? await reconcileTeamNotifications(dir, config)
|
|
2534
|
+
: summarizeNotifications(await listNotificationRecords(dir));
|
|
1740
2535
|
const phase = await resolveGjcTeamSnapshotPhase(dir, config, storedPhase, tasks, monitor);
|
|
1741
2536
|
return {
|
|
1742
2537
|
team_name: config.team_name,
|
|
@@ -1750,10 +2545,19 @@ export async function readGjcTeamSnapshot(
|
|
|
1750
2545
|
task_counts: taskCounts,
|
|
1751
2546
|
workers: config.workers,
|
|
1752
2547
|
integration_by_worker: monitor?.integration_by_worker,
|
|
2548
|
+
worker_lifecycle_by_id: workerLifecycleById,
|
|
1753
2549
|
notification_summary: notificationSummary,
|
|
1754
2550
|
updated_at: config.updated_at,
|
|
1755
2551
|
};
|
|
1756
2552
|
}
|
|
2553
|
+
export async function monitorGjcTeamSnapshot(
|
|
2554
|
+
teamName: string,
|
|
2555
|
+
cwd = process.cwd(),
|
|
2556
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
2557
|
+
): Promise<GjcTeamSnapshot> {
|
|
2558
|
+
const snapshot = await monitorGjcTeam(teamName, cwd, env);
|
|
2559
|
+
return snapshot;
|
|
2560
|
+
}
|
|
1757
2561
|
function workerIntegrationFingerprint(head: string | null, classification: GjcWorkerCheckpointClassification): string {
|
|
1758
2562
|
return `${head ?? "no-head"}:${classification.kind}:${classification.files.join("\0")}`;
|
|
1759
2563
|
}
|
|
@@ -1864,6 +2668,7 @@ export async function monitorGjcTeam(
|
|
|
1864
2668
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
1865
2669
|
const config = await readConfig(dir);
|
|
1866
2670
|
const previous = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
|
|
2671
|
+
await reconcileGjcTeamStaleClaims(teamName, dir, config, env);
|
|
1867
2672
|
const integrationByWorker = await integrateGjcWorkerCommits(config, dir, previous, cwd, env);
|
|
1868
2673
|
await writeJsonFile(monitorSnapshotPath(dir), { integration_by_worker: integrationByWorker, updated_at: now() });
|
|
1869
2674
|
await replayGjcTeamNotifications(teamName, cwd, env);
|
|
@@ -1902,7 +2707,7 @@ async function writeGjcWorkerStartupAck(
|
|
|
1902
2707
|
): Promise<Record<string, unknown>> {
|
|
1903
2708
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
1904
2709
|
const config = await readConfig(dir);
|
|
1905
|
-
|
|
2710
|
+
const teamWorker = findKnownWorker(config, worker);
|
|
1906
2711
|
const ack = {
|
|
1907
2712
|
worker,
|
|
1908
2713
|
pid: typeof input.pid === "number" ? input.pid : undefined,
|
|
@@ -1911,6 +2716,11 @@ async function writeGjcWorkerStartupAck(
|
|
|
1911
2716
|
ack_at: now(),
|
|
1912
2717
|
};
|
|
1913
2718
|
await writeJsonFile(path.join(workerDir(dir, worker), "startup-ack.json"), ack);
|
|
2719
|
+
await writeWorkerLifecycleRecord(dir, teamWorker, "ready", {
|
|
2720
|
+
pane_id: teamWorker.pane_id,
|
|
2721
|
+
pid: typeof input.pid === "number" ? input.pid : undefined,
|
|
2722
|
+
started_at: ack.ack_at,
|
|
2723
|
+
});
|
|
1914
2724
|
await appendEvent(dir, { type: "worker_startup_ack", worker, message: `Worker ${worker} acknowledged startup` });
|
|
1915
2725
|
return ack;
|
|
1916
2726
|
}
|
|
@@ -2026,12 +2836,31 @@ export async function shutdownGjcTeam(
|
|
|
2026
2836
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2027
2837
|
const config = await readConfig(dir);
|
|
2028
2838
|
const tasks = await readTasks(dir);
|
|
2029
|
-
const
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2839
|
+
const evidenceFailures = tasks
|
|
2840
|
+
.map(task => {
|
|
2841
|
+
const reason = task.status === "completed" ? getGjcTeamTaskCompletionEvidenceFailure(task) : null;
|
|
2842
|
+
return reason ? { task_id: task.id, reason } : null;
|
|
2843
|
+
})
|
|
2844
|
+
.filter((failure): failure is { task_id: string; reason: string } => failure != null);
|
|
2845
|
+
const shutdownRequestId = `shutdown-${stableHash([config.team_name, now(), randomUUID()].join(":"))}`;
|
|
2846
|
+
const shutdownRequestedAt = now();
|
|
2847
|
+
await Promise.all(
|
|
2848
|
+
config.workers.map(worker =>
|
|
2849
|
+
writeGjcShutdownRequest(
|
|
2850
|
+
teamName,
|
|
2851
|
+
worker.id,
|
|
2852
|
+
"leader-fixed",
|
|
2853
|
+
cwd,
|
|
2854
|
+
env,
|
|
2855
|
+
shutdownRequestId,
|
|
2856
|
+
"graceful",
|
|
2857
|
+
shutdownRequestedAt,
|
|
2858
|
+
),
|
|
2859
|
+
),
|
|
2860
|
+
);
|
|
2861
|
+
const monitor = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
|
|
2862
|
+
const completionVerified = tasks.length === 0 || tasks.every(isGjcTeamTaskCompletionVerified);
|
|
2863
|
+
const pendingIntegration = completionVerified ? await hasPendingGjcTeamIntegration(dir, config, monitor) : false;
|
|
2035
2864
|
killWorkerPanes(config);
|
|
2036
2865
|
await removeCleanCreatedWorktrees(config.workers);
|
|
2037
2866
|
const stopped = {
|
|
@@ -2040,18 +2869,50 @@ export async function shutdownGjcTeam(
|
|
|
2040
2869
|
updated_at: now(),
|
|
2041
2870
|
};
|
|
2042
2871
|
await writeJsonFile(path.join(dir, "config.json"), stopped);
|
|
2872
|
+
await writeWorkerLifecycleForConfig(dir, stopped, "stopped", worker => ({
|
|
2873
|
+
pane_id: worker.pane_id,
|
|
2874
|
+
stopped_at: stopped.updated_at,
|
|
2875
|
+
stop_reason: "graceful_shutdown",
|
|
2876
|
+
shutdown_request_id: shutdownRequestId,
|
|
2877
|
+
shutdown_requested_at: shutdownRequestedAt,
|
|
2878
|
+
shutdown_mode: "graceful",
|
|
2879
|
+
}));
|
|
2880
|
+
const workerLifecycleById = await readWorkerLifecycleById(dir, stopped);
|
|
2881
|
+
const gracefulShutdownComplete = stopped.workers.every(worker => {
|
|
2882
|
+
const lifecycle = workerLifecycleById[worker.id];
|
|
2883
|
+
return (
|
|
2884
|
+
lifecycle?.lifecycle_state === "stopped" &&
|
|
2885
|
+
lifecycle.shutdown_request_id === shutdownRequestId &&
|
|
2886
|
+
lifecycle.shutdown_mode === "graceful"
|
|
2887
|
+
);
|
|
2888
|
+
});
|
|
2889
|
+
const shutdownPhase: GjcTeamPhase =
|
|
2890
|
+
completionVerified && gracefulShutdownComplete
|
|
2891
|
+
? pendingIntegration
|
|
2892
|
+
? "awaiting_integration"
|
|
2893
|
+
: "complete"
|
|
2894
|
+
: evidenceFailures.length > 0 || tasks.some(task => task.status === "failed" || task.status === "blocked")
|
|
2895
|
+
? "failed"
|
|
2896
|
+
: "cancelled";
|
|
2043
2897
|
await writePhase(dir, shutdownPhase);
|
|
2898
|
+
const shutdownData: Record<string, unknown> = {
|
|
2899
|
+
phase: shutdownPhase,
|
|
2900
|
+
shutdown_request_id: shutdownRequestId,
|
|
2901
|
+
graceful_shutdown_complete: gracefulShutdownComplete,
|
|
2902
|
+
};
|
|
2903
|
+
if (evidenceFailures.length > 0) shutdownData.evidence_failures = evidenceFailures;
|
|
2044
2904
|
await appendEvent(dir, {
|
|
2045
2905
|
type: "team_shutdown",
|
|
2046
2906
|
message:
|
|
2047
2907
|
shutdownPhase === "complete"
|
|
2048
2908
|
? "Shut down native gjc team runtime after completed tasks"
|
|
2049
2909
|
: "Shut down native gjc team runtime with incomplete tasks",
|
|
2050
|
-
data:
|
|
2910
|
+
data: shutdownData,
|
|
2051
2911
|
});
|
|
2052
2912
|
await appendTelemetry(dir, {
|
|
2053
2913
|
type: "team_shutdown",
|
|
2054
2914
|
message: `Native gjc team runtime stopped with phase ${shutdownPhase}`,
|
|
2915
|
+
data: { shutdown_request_id: shutdownRequestId, graceful_shutdown_complete: gracefulShutdownComplete },
|
|
2055
2916
|
});
|
|
2056
2917
|
return readGjcTeamSnapshot(config.team_name, cwd, env);
|
|
2057
2918
|
}
|
|
@@ -2079,9 +2940,11 @@ export async function createGjcTeamTask(
|
|
|
2079
2940
|
description: string,
|
|
2080
2941
|
cwd = process.cwd(),
|
|
2081
2942
|
env: NodeJS.ProcessEnv = process.env,
|
|
2943
|
+
taskOptions: GjcTeamTaskMetadataInput = {},
|
|
2082
2944
|
): Promise<GjcTeamTask> {
|
|
2083
2945
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2084
2946
|
const config = await readConfig(dir);
|
|
2947
|
+
if (taskOptions.owner) assertKnownWorker(config, taskOptions.owner);
|
|
2085
2948
|
const tasks = await readTasks(dir);
|
|
2086
2949
|
const next = tasks.length + 1;
|
|
2087
2950
|
const task: GjcTeamTask = {
|
|
@@ -2091,6 +2954,12 @@ export async function createGjcTeamTask(
|
|
|
2091
2954
|
title: subject,
|
|
2092
2955
|
objective: description,
|
|
2093
2956
|
status: "pending",
|
|
2957
|
+
...(taskOptions.owner ? { owner: taskOptions.owner } : {}),
|
|
2958
|
+
...(taskOptions.lane ? { lane: taskOptions.lane } : {}),
|
|
2959
|
+
...(taskOptions.required_role ? { required_role: taskOptions.required_role } : {}),
|
|
2960
|
+
...(taskOptions.allowed_roles ? { allowed_roles: taskOptions.allowed_roles } : {}),
|
|
2961
|
+
...(taskOptions.depends_on ? { depends_on: taskOptions.depends_on } : {}),
|
|
2962
|
+
...(taskOptions.blocked_by ? { blocked_by: taskOptions.blocked_by } : {}),
|
|
2094
2963
|
version: 1,
|
|
2095
2964
|
created_at: now(),
|
|
2096
2965
|
updated_at: now(),
|
|
@@ -2104,7 +2973,12 @@ export async function createGjcTeamTask(
|
|
|
2104
2973
|
export async function updateGjcTeamTask(
|
|
2105
2974
|
teamName: string,
|
|
2106
2975
|
taskId: string,
|
|
2107
|
-
updates: Partial<
|
|
2976
|
+
updates: Partial<
|
|
2977
|
+
Pick<
|
|
2978
|
+
GjcTeamTask,
|
|
2979
|
+
"subject" | "description" | "blocked_by" | "depends_on" | "lane" | "required_role" | "allowed_roles"
|
|
2980
|
+
>
|
|
2981
|
+
>,
|
|
2108
2982
|
cwd = process.cwd(),
|
|
2109
2983
|
env: NodeJS.ProcessEnv = process.env,
|
|
2110
2984
|
): Promise<GjcTeamTask> {
|
|
@@ -2131,13 +3005,20 @@ export async function claimGjcTeamTask(
|
|
|
2131
3005
|
): Promise<GjcTeamApiClaimResult> {
|
|
2132
3006
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2133
3007
|
const config = await readConfig(dir);
|
|
2134
|
-
|
|
3008
|
+
const teamWorker = findKnownWorker(config, workerId);
|
|
3009
|
+
const livenessRecovery = await reconcileGjcTeamStaleClaims(teamName, dir, config, env);
|
|
3010
|
+
const staleWorkerReasons = livenessRecovery.stale_workers[workerId];
|
|
3011
|
+
if (staleWorkerReasons?.length)
|
|
3012
|
+
return { ok: false, reason: `worker_not_live:${workerId}:${staleWorkerReasons.join(",")}` };
|
|
2135
3013
|
const tasks = await readTasks(dir);
|
|
2136
3014
|
const task = taskId
|
|
2137
3015
|
? tasks.find(candidate => candidate.id === taskId)
|
|
2138
|
-
: tasks.find(candidate =>
|
|
2139
|
-
if (!task) return { ok: false, reason: "no_pending_task" };
|
|
2140
|
-
|
|
3016
|
+
: tasks.find(candidate => getGjcTeamTaskClaimEligibilityReason(candidate, teamWorker, tasks) == null);
|
|
3017
|
+
if (!task) return { ok: false, reason: taskId ? `task_not_found:${taskId}` : "no_pending_task" };
|
|
3018
|
+
const eligibilityReason = getGjcTeamTaskClaimEligibilityReason(task, teamWorker, tasks);
|
|
3019
|
+
if (eligibilityReason) return { ok: false, reason: eligibilityReason };
|
|
3020
|
+
const activeClaimReason = await getActiveClaimReason(dir, task);
|
|
3021
|
+
if (activeClaimReason) return { ok: false, reason: activeClaimReason };
|
|
2141
3022
|
const token = randomUUID();
|
|
2142
3023
|
const claim: GjcTeamTaskClaim = {
|
|
2143
3024
|
owner: workerId,
|
|
@@ -2145,20 +3026,19 @@ export async function claimGjcTeamTask(
|
|
|
2145
3026
|
leased_until: new Date(Date.now() + 30 * 60_000).toISOString(),
|
|
2146
3027
|
};
|
|
2147
3028
|
const claimPath = path.join(dir, "claims", `${task.id}.json`);
|
|
2148
|
-
await
|
|
2149
|
-
|
|
2150
|
-
try {
|
|
2151
|
-
claimFile = await fs.open(claimPath, "wx");
|
|
2152
|
-
await claimFile.writeFile(`${JSON.stringify(claim, null, 2)}\n`, "utf-8");
|
|
2153
|
-
} catch (error) {
|
|
2154
|
-
if (isEexist(error)) return { ok: false, reason: `task_already_claimed:${task.id}` };
|
|
2155
|
-
throw error;
|
|
2156
|
-
} finally {
|
|
2157
|
-
await claimFile?.close();
|
|
2158
|
-
}
|
|
3029
|
+
const created = await writeJsonFileNoClobber(claimPath, claim);
|
|
3030
|
+
if (!created) return { ok: false, reason: `task_already_claimed:${task.id}` };
|
|
2159
3031
|
const current = await readGjcTeamTask(teamName, task.id, cwd, env);
|
|
2160
|
-
|
|
3032
|
+
const currentEligibilityReason = getGjcTeamTaskClaimEligibilityReason(current, teamWorker, await readTasks(dir));
|
|
3033
|
+
if (currentEligibilityReason) {
|
|
2161
3034
|
await fs.rm(claimPath, { force: true });
|
|
3035
|
+
return { ok: false, reason: currentEligibilityReason };
|
|
3036
|
+
}
|
|
3037
|
+
if (current.status !== "pending") {
|
|
3038
|
+
await deleteIfOwned(claimPath, {
|
|
3039
|
+
...stateWriterOptions(claimPath, "prune", "rollback"),
|
|
3040
|
+
predicate: current => (current as GjcTeamTaskClaim).token === token,
|
|
3041
|
+
});
|
|
2162
3042
|
return { ok: false, reason: `task_not_pending:${task.id}` };
|
|
2163
3043
|
}
|
|
2164
3044
|
const updated: GjcTeamTask = {
|
|
@@ -2173,7 +3053,10 @@ export async function claimGjcTeamTask(
|
|
|
2173
3053
|
try {
|
|
2174
3054
|
await writeTask(dir, updated);
|
|
2175
3055
|
} catch (error) {
|
|
2176
|
-
await
|
|
3056
|
+
await deleteIfOwned(claimPath, {
|
|
3057
|
+
...stateWriterOptions(claimPath, "prune", "rollback"),
|
|
3058
|
+
predicate: current => (current as GjcTeamTaskClaim).token === token,
|
|
3059
|
+
});
|
|
2177
3060
|
throw error;
|
|
2178
3061
|
}
|
|
2179
3062
|
await appendEvent(dir, {
|
|
@@ -2192,7 +3075,7 @@ export async function transitionGjcTeamTaskStatus(
|
|
|
2192
3075
|
env: NodeJS.ProcessEnv = process.env,
|
|
2193
3076
|
claimToken?: string,
|
|
2194
3077
|
workerId?: string,
|
|
2195
|
-
|
|
3078
|
+
completionEvidenceInput?: unknown,
|
|
2196
3079
|
): Promise<GjcTeamTask> {
|
|
2197
3080
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2198
3081
|
const config = await readConfig(dir);
|
|
@@ -2205,30 +3088,39 @@ export async function transitionGjcTeamTaskStatus(
|
|
|
2205
3088
|
if (task.claim.token !== claimToken) throw new Error(`claim_token_mismatch:${taskId}`);
|
|
2206
3089
|
if (workerId && task.claim.owner !== workerId) throw new Error(`claim_owner_mismatch:${taskId}`);
|
|
2207
3090
|
const terminal = status === "completed" || status === "failed";
|
|
2208
|
-
|
|
2209
|
-
|
|
3091
|
+
const transitionedAt = now();
|
|
3092
|
+
const completionEvidence =
|
|
3093
|
+
status === "completed"
|
|
3094
|
+
? normalizeGjcTeamTaskCompletionEvidence(taskId, task.claim.owner, completionEvidenceInput, transitionedAt)
|
|
3095
|
+
: undefined;
|
|
2210
3096
|
const updated: GjcTeamTask = {
|
|
2211
3097
|
...task,
|
|
2212
3098
|
status,
|
|
2213
3099
|
claim: terminal ? undefined : task.claim,
|
|
2214
3100
|
version: task.version + 1,
|
|
2215
|
-
updated_at:
|
|
2216
|
-
...(terminal ? { completed_at:
|
|
3101
|
+
updated_at: transitionedAt,
|
|
3102
|
+
...(terminal ? { completed_at: transitionedAt } : {}),
|
|
3103
|
+
...(completionEvidence ? { completion_evidence: completionEvidence } : {}),
|
|
2217
3104
|
};
|
|
2218
3105
|
await writeTask(dir, updated);
|
|
2219
|
-
if (terminal
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
3106
|
+
if (terminal) {
|
|
3107
|
+
const claimPath = path.join(dir, "claims", `${taskId}.json`);
|
|
3108
|
+
await removeFileAudited(claimPath, stateWriterOptions(claimPath, "prune", "terminal"));
|
|
3109
|
+
}
|
|
3110
|
+
const eventData: Record<string, unknown> = { status };
|
|
3111
|
+
if (completionEvidence) {
|
|
3112
|
+
eventData.completion_evidence = {
|
|
3113
|
+
recorded_by: completionEvidence.recorded_by,
|
|
3114
|
+
item_count: completionEvidence.items.length,
|
|
3115
|
+
verified_item_count: completionEvidence.items.filter(isGjcTeamCompletionEvidenceItemVerified).length,
|
|
3116
|
+
files_count: completionEvidence.files?.length ?? 0,
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
2227
3119
|
await appendEvent(dir, {
|
|
2228
3120
|
type: "task_transitioned",
|
|
2229
3121
|
task_id: taskId,
|
|
2230
3122
|
message: "Task status changed",
|
|
2231
|
-
data:
|
|
3123
|
+
data: eventData,
|
|
2232
3124
|
});
|
|
2233
3125
|
return updated;
|
|
2234
3126
|
}
|
|
@@ -2239,8 +3131,18 @@ export async function transitionGjcTeamTask(
|
|
|
2239
3131
|
cwd = process.cwd(),
|
|
2240
3132
|
env: NodeJS.ProcessEnv = process.env,
|
|
2241
3133
|
claimToken?: string,
|
|
3134
|
+
completionEvidenceInput?: unknown,
|
|
2242
3135
|
): Promise<GjcTeamTask> {
|
|
2243
|
-
return transitionGjcTeamTaskStatus(
|
|
3136
|
+
return transitionGjcTeamTaskStatus(
|
|
3137
|
+
teamName,
|
|
3138
|
+
taskId,
|
|
3139
|
+
parseGjcTeamTaskStatus(status, true),
|
|
3140
|
+
cwd,
|
|
3141
|
+
env,
|
|
3142
|
+
claimToken,
|
|
3143
|
+
undefined,
|
|
3144
|
+
completionEvidenceInput,
|
|
3145
|
+
);
|
|
2244
3146
|
}
|
|
2245
3147
|
export async function releaseGjcTeamTaskClaim(
|
|
2246
3148
|
teamName: string,
|
|
@@ -2263,7 +3165,11 @@ export async function releaseGjcTeamTaskClaim(
|
|
|
2263
3165
|
updated_at: now(),
|
|
2264
3166
|
};
|
|
2265
3167
|
await writeTask(dir, updated);
|
|
2266
|
-
|
|
3168
|
+
const claimPath = path.join(dir, "claims", `${taskId}.json`);
|
|
3169
|
+
await deleteIfOwned(claimPath, {
|
|
3170
|
+
...stateWriterOptions(claimPath, "prune", "release"),
|
|
3171
|
+
predicate: current => (current as GjcTeamTaskClaim).token === claimToken,
|
|
3172
|
+
});
|
|
2267
3173
|
await appendEvent(dir, {
|
|
2268
3174
|
type: "task_claim_released",
|
|
2269
3175
|
task_id: taskId,
|
|
@@ -2603,12 +3509,43 @@ export async function readGjcWorkerStatus(
|
|
|
2603
3509
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2604
3510
|
const config = await readConfig(dir);
|
|
2605
3511
|
assertKnownWorker(config, worker);
|
|
2606
|
-
return (
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
3512
|
+
return readWorkerStatusFile(dir, worker);
|
|
3513
|
+
}
|
|
3514
|
+
export async function updateGjcWorkerStatus(
|
|
3515
|
+
teamName: string,
|
|
3516
|
+
worker: string,
|
|
3517
|
+
status: GjcWorkerStatusState,
|
|
3518
|
+
cwd = process.cwd(),
|
|
3519
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
3520
|
+
currentTaskId?: string,
|
|
3521
|
+
reason?: string,
|
|
3522
|
+
): Promise<WorkerStatusFile> {
|
|
3523
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
3524
|
+
const config = await readConfig(dir);
|
|
3525
|
+
const teamWorker = findKnownWorker(config, worker);
|
|
3526
|
+
if (currentTaskId) assertSafeId("task_id", currentTaskId);
|
|
3527
|
+
const trimmedReason = reason?.trim();
|
|
3528
|
+
const value: WorkerStatusFile = {
|
|
3529
|
+
state: status,
|
|
3530
|
+
...(currentTaskId ? { current_task_id: currentTaskId } : {}),
|
|
3531
|
+
...(trimmedReason ? { reason: trimmedReason } : {}),
|
|
3532
|
+
updated_at: now(),
|
|
3533
|
+
};
|
|
3534
|
+
await writeJsonFile(path.join(workerDir(dir, worker), "status.json"), value);
|
|
3535
|
+
const currentLifecycle = await readWorkerLifecycleRecord(dir, teamWorker);
|
|
3536
|
+
const lifecycleState =
|
|
3537
|
+
currentLifecycle.lifecycle_state === "stopped" ? "stopped" : lifecycleStateForWorkerStatus(status);
|
|
3538
|
+
await writeWorkerLifecycleRecord(dir, teamWorker, lifecycleState);
|
|
3539
|
+
await appendEvent(dir, {
|
|
3540
|
+
type: "worker_status_updated",
|
|
3541
|
+
worker,
|
|
3542
|
+
message: `Worker ${worker} reported ${status}`,
|
|
3543
|
+
data: {
|
|
3544
|
+
status,
|
|
3545
|
+
current_task_id: currentTaskId,
|
|
3546
|
+
},
|
|
3547
|
+
});
|
|
3548
|
+
return value;
|
|
2612
3549
|
}
|
|
2613
3550
|
export async function readGjcWorkerHeartbeat(
|
|
2614
3551
|
teamName: string,
|
|
@@ -2646,7 +3583,7 @@ export async function writeGjcWorkerInbox(
|
|
|
2646
3583
|
const config = await readConfig(dir);
|
|
2647
3584
|
assertKnownWorker(config, worker);
|
|
2648
3585
|
const filePath = path.join(workerDir(dir, worker), "inbox.md");
|
|
2649
|
-
await
|
|
3586
|
+
await writeReport(filePath, content, stateWriterOptions(filePath, "report", "write"));
|
|
2650
3587
|
return { path: filePath };
|
|
2651
3588
|
}
|
|
2652
3589
|
export async function writeGjcWorkerIdentity(
|
|
@@ -2678,6 +3615,23 @@ export async function readGjcTeamEvents(
|
|
|
2678
3615
|
throw error;
|
|
2679
3616
|
}
|
|
2680
3617
|
}
|
|
3618
|
+
export async function readGjcTeamTraces(
|
|
3619
|
+
teamName: string,
|
|
3620
|
+
cwd = process.cwd(),
|
|
3621
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
3622
|
+
): Promise<GjcTeamTraceEvent[]> {
|
|
3623
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
3624
|
+
try {
|
|
3625
|
+
const text = await Bun.file(tracePath(dir)).text();
|
|
3626
|
+
return text
|
|
3627
|
+
.split(/\r?\n/)
|
|
3628
|
+
.filter(Boolean)
|
|
3629
|
+
.map(line => JSON.parse(line) as GjcTeamTraceEvent);
|
|
3630
|
+
} catch (error) {
|
|
3631
|
+
if (isEnoent(error)) return [];
|
|
3632
|
+
throw error;
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
2681
3635
|
export async function appendGjcTeamEvent(
|
|
2682
3636
|
teamName: string,
|
|
2683
3637
|
type: string,
|
|
@@ -2741,13 +3695,27 @@ export async function writeGjcShutdownRequest(
|
|
|
2741
3695
|
requestedBy: string,
|
|
2742
3696
|
cwd = process.cwd(),
|
|
2743
3697
|
env: NodeJS.ProcessEnv = process.env,
|
|
3698
|
+
requestId = `shutdown-${stableHash([teamName, worker, now(), randomUUID()].join(":"))}`,
|
|
3699
|
+
mode: GjcTeamShutdownMode = "graceful",
|
|
3700
|
+
requestedAt = now(),
|
|
2744
3701
|
): Promise<Record<string, unknown>> {
|
|
2745
3702
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2746
3703
|
const config = await readConfig(dir);
|
|
2747
|
-
|
|
3704
|
+
const teamWorker = findKnownWorker(config, worker);
|
|
2748
3705
|
assertKnownParticipant(config, requestedBy);
|
|
2749
|
-
const value = { worker, requested_by: requestedBy, requested_at:
|
|
3706
|
+
const value = { worker, requested_by: requestedBy, request_id: requestId, mode, requested_at: requestedAt };
|
|
2750
3707
|
await writeJsonFile(path.join(workerDir(dir, worker), "shutdown-request.json"), value);
|
|
3708
|
+
await writeWorkerLifecycleRecord(dir, teamWorker, "draining", {
|
|
3709
|
+
shutdown_request_id: requestId,
|
|
3710
|
+
shutdown_requested_at: requestedAt,
|
|
3711
|
+
shutdown_mode: mode,
|
|
3712
|
+
});
|
|
3713
|
+
await appendEvent(dir, {
|
|
3714
|
+
type: "worker_shutdown_requested",
|
|
3715
|
+
worker,
|
|
3716
|
+
message: `Worker ${worker} shutdown requested`,
|
|
3717
|
+
data: { requested_by: requestedBy, request_id: requestId, mode },
|
|
3718
|
+
});
|
|
2751
3719
|
return value;
|
|
2752
3720
|
}
|
|
2753
3721
|
export async function readGjcShutdownAck(
|
|
@@ -2786,6 +3754,7 @@ export async function executeGjcTeamApiOperation(
|
|
|
2786
3754
|
String(input.description ?? ""),
|
|
2787
3755
|
cwd,
|
|
2788
3756
|
env,
|
|
3757
|
+
taskMetadataFromInput(input, true),
|
|
2789
3758
|
),
|
|
2790
3759
|
};
|
|
2791
3760
|
case "update-task":
|
|
@@ -2796,19 +3765,22 @@ export async function executeGjcTeamApiOperation(
|
|
|
2796
3765
|
{
|
|
2797
3766
|
subject: typeof input.subject === "string" ? input.subject : undefined,
|
|
2798
3767
|
description: typeof input.description === "string" ? input.description : undefined,
|
|
3768
|
+
...taskMetadataFromInput(input),
|
|
2799
3769
|
},
|
|
2800
3770
|
cwd,
|
|
2801
3771
|
env,
|
|
2802
3772
|
),
|
|
2803
3773
|
};
|
|
2804
|
-
case "claim-task":
|
|
3774
|
+
case "claim-task": {
|
|
3775
|
+
const requestedTaskId = input.task_id ?? input.taskId;
|
|
2805
3776
|
return claimGjcTeamTask(
|
|
2806
3777
|
teamName,
|
|
2807
3778
|
worker,
|
|
2808
3779
|
cwd,
|
|
2809
3780
|
env,
|
|
2810
|
-
typeof
|
|
3781
|
+
typeof requestedTaskId === "string" ? requestedTaskId : undefined,
|
|
2811
3782
|
);
|
|
3783
|
+
}
|
|
2812
3784
|
case "transition-task":
|
|
2813
3785
|
case "transition-task-status":
|
|
2814
3786
|
return {
|
|
@@ -2821,11 +3793,7 @@ export async function executeGjcTeamApiOperation(
|
|
|
2821
3793
|
env,
|
|
2822
3794
|
typeof input.claim_token === "string" ? input.claim_token : undefined,
|
|
2823
3795
|
explicitWorker,
|
|
2824
|
-
|
|
2825
|
-
? input.evidence
|
|
2826
|
-
: typeof input.result === "string"
|
|
2827
|
-
? input.result
|
|
2828
|
-
: undefined,
|
|
3796
|
+
input.completion_evidence ?? input.completionEvidence,
|
|
2829
3797
|
),
|
|
2830
3798
|
};
|
|
2831
3799
|
case "release-task-claim":
|
|
@@ -2925,8 +3893,22 @@ export async function executeGjcTeamApiOperation(
|
|
|
2925
3893
|
return readJsonFile(path.join(await findTeamDir(teamName, cwd, env), "manifest.v2.json"));
|
|
2926
3894
|
case "read-worker-status":
|
|
2927
3895
|
return readGjcWorkerStatus(teamName, worker, cwd, env);
|
|
3896
|
+
case "update-worker-status": {
|
|
3897
|
+
const currentTaskIdInput = input.current_task_id ?? input.currentTaskId;
|
|
3898
|
+
return updateGjcWorkerStatus(
|
|
3899
|
+
teamName,
|
|
3900
|
+
worker,
|
|
3901
|
+
parseRequiredGjcWorkerStatusState(input.status ?? input.state),
|
|
3902
|
+
cwd,
|
|
3903
|
+
env,
|
|
3904
|
+
typeof currentTaskIdInput === "string" ? currentTaskIdInput : undefined,
|
|
3905
|
+
typeof input.reason === "string" ? input.reason : undefined,
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
2928
3908
|
case "read-worker-heartbeat":
|
|
2929
3909
|
return readGjcWorkerHeartbeat(teamName, worker, cwd, env);
|
|
3910
|
+
case "recover-stale-claims":
|
|
3911
|
+
return recoverGjcTeamStaleClaims(teamName, cwd, env);
|
|
2930
3912
|
case "update-worker-heartbeat":
|
|
2931
3913
|
return updateGjcWorkerHeartbeat(
|
|
2932
3914
|
teamName,
|
|
@@ -2962,6 +3944,8 @@ export async function executeGjcTeamApiOperation(
|
|
|
2962
3944
|
return appendGjcTeamEvent(teamName, String(input.type ?? "event"), worker, cwd, env);
|
|
2963
3945
|
case "read-events":
|
|
2964
3946
|
return { events: await readGjcTeamEvents(teamName, cwd, env) };
|
|
3947
|
+
case "read-traces":
|
|
3948
|
+
return { traces: await readGjcTeamTraces(teamName, cwd, env) };
|
|
2965
3949
|
case "await-event":
|
|
2966
3950
|
return awaitGjcTeamEvent(teamName, Number(input.timeout_ms ?? 0), cwd, env);
|
|
2967
3951
|
case "write-monitor-snapshot":
|
|
@@ -2972,8 +3956,18 @@ export async function executeGjcTeamApiOperation(
|
|
|
2972
3956
|
return writeGjcTaskApproval(teamName, String(input.task_id), input, cwd, env);
|
|
2973
3957
|
case "read-task-approval":
|
|
2974
3958
|
return readGjcTaskApproval(teamName, String(input.task_id), cwd, env);
|
|
2975
|
-
case "write-shutdown-request":
|
|
2976
|
-
|
|
3959
|
+
case "write-shutdown-request": {
|
|
3960
|
+
const shutdownRequestIdInput = input.request_id ?? input.requestId;
|
|
3961
|
+
return writeGjcShutdownRequest(
|
|
3962
|
+
teamName,
|
|
3963
|
+
worker,
|
|
3964
|
+
String(input.requested_by ?? input.requestedBy ?? "leader-fixed"),
|
|
3965
|
+
cwd,
|
|
3966
|
+
env,
|
|
3967
|
+
typeof shutdownRequestIdInput === "string" ? shutdownRequestIdInput : undefined,
|
|
3968
|
+
parseGjcTeamShutdownMode(input.mode),
|
|
3969
|
+
);
|
|
3970
|
+
}
|
|
2977
3971
|
case "read-shutdown-ack":
|
|
2978
3972
|
return readGjcShutdownAck(teamName, worker, cwd, env);
|
|
2979
3973
|
default:
|