@gajae-code/coding-agent 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/config/keybindings.d.ts +4 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
- package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +36 -7
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +7 -4
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
- package/dist/types/harness-control-plane/storage.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +12 -4
- package/dist/types/migrate/action-planner.d.ts +11 -0
- package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
- package/dist/types/migrate/adapters/codex.d.ts +5 -0
- package/dist/types/migrate/adapters/index.d.ts +45 -0
- package/dist/types/migrate/adapters/opencode.d.ts +2 -0
- package/dist/types/migrate/executor.d.ts +2 -0
- package/dist/types/migrate/mcp-mapper.d.ts +20 -0
- package/dist/types/migrate/report.d.ts +18 -0
- package/dist/types/migrate/skill-normalizer.d.ts +27 -0
- package/dist/types/migrate/types.d.ts +126 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
- package/dist/types/research-plan/index.d.ts +1 -0
- package/dist/types/research-plan/ledger.d.ts +33 -0
- package/dist/types/rlm/artifacts.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
- package/dist/types/skill-state/active-state.d.ts +6 -11
- package/dist/types/skill-state/canonical-skills.d.ts +3 -0
- package/dist/types/skill-state/workflow-hud.d.ts +2 -0
- package/dist/types/task/spawn-gate.d.ts +1 -10
- package/package.json +7 -7
- package/src/cli/migrate-cli.ts +106 -0
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +2 -2
- package/src/commands/migrate.ts +46 -0
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +7 -3
- package/src/coordinator-mcp/policy.ts +10 -2
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
- package/src/defaults/gjc/skills/team/SKILL.md +51 -47
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +17 -13
- package/src/extensibility/custom-commands/loader.ts +0 -7
- package/src/extensibility/gjc-plugins/injection.ts +23 -4
- package/src/extensibility/gjc-plugins/state.ts +16 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +43 -18
- package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
- package/src/gjc-runtime/goal-mode-request.ts +26 -11
- package/src/gjc-runtime/launch-tmux.ts +6 -1
- package/src/gjc-runtime/ralplan-runtime.ts +79 -50
- package/src/gjc-runtime/session-layout.ts +180 -0
- package/src/gjc-runtime/session-resolution.ts +217 -0
- package/src/gjc-runtime/state-graph.ts +1 -2
- package/src/gjc-runtime/state-migrations.ts +1 -0
- package/src/gjc-runtime/state-runtime.ts +230 -121
- package/src/gjc-runtime/state-schema.ts +2 -0
- package/src/gjc-runtime/state-writer.ts +289 -41
- package/src/gjc-runtime/team-runtime.ts +43 -19
- package/src/gjc-runtime/tmux-sessions.ts +7 -1
- package/src/gjc-runtime/ultragoal-guard.ts +45 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +121 -41
- package/src/gjc-runtime/workflow-command-ref.ts +1 -2
- package/src/gjc-runtime/workflow-manifest.ts +1 -2
- package/src/harness-control-plane/storage.ts +14 -4
- package/src/hooks/native-skill-hook.ts +38 -12
- package/src/hooks/skill-state.ts +178 -83
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/migrate/action-planner.ts +318 -0
- package/src/migrate/adapters/claude-code.ts +39 -0
- package/src/migrate/adapters/codex.ts +70 -0
- package/src/migrate/adapters/index.ts +277 -0
- package/src/migrate/adapters/opencode.ts +52 -0
- package/src/migrate/executor.ts +81 -0
- package/src/migrate/mcp-mapper.ts +152 -0
- package/src/migrate/report.ts +104 -0
- package/src/migrate/skill-normalizer.ts +80 -0
- package/src/migrate/types.ts +163 -0
- package/src/modes/bridge/bridge-mode.ts +2 -2
- package/src/modes/components/custom-editor.ts +30 -20
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -2
- package/src/research-plan/index.ts +1 -0
- package/src/research-plan/ledger.ts +177 -0
- package/src/rlm/artifacts.ts +12 -3
- package/src/rlm/index.ts +7 -0
- package/src/runtime-mcp/config-writer.ts +46 -0
- package/src/session/agent-session.ts +15 -21
- package/src/setup/hermes-setup.ts +1 -1
- package/src/skill-state/active-state.ts +72 -108
- package/src/skill-state/canonical-skills.ts +4 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
- package/src/skill-state/workflow-hud.ts +4 -2
- package/src/skill-state/workflow-state-contract.ts +3 -3
- package/src/task/agents.ts +1 -22
- package/src/task/index.ts +1 -41
- package/src/task/spawn-gate.ts +1 -38
- package/src/task/types.ts +1 -1
- package/src/tools/ask.ts +34 -12
- package/src/tools/computer.ts +58 -4
- package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
- package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
- package/src/prompts/agents/explore.md +0 -58
- package/src/prompts/agents/plan.md +0 -49
- package/src/prompts/agents/reviewer.md +0 -141
- package/src/prompts/agents/task.md +0 -16
- package/src/prompts/review-request.md +0 -70
|
@@ -60,6 +60,7 @@ export const WorkflowStateEnvelopeSchema = z
|
|
|
60
60
|
updated_at: z.string().optional(),
|
|
61
61
|
session_id: z.string().optional(),
|
|
62
62
|
receipt: WorkflowStateReceiptSchema.optional(),
|
|
63
|
+
state_revision: z.number().optional(),
|
|
63
64
|
})
|
|
64
65
|
.passthrough();
|
|
65
66
|
|
|
@@ -95,6 +96,7 @@ export const RequiredOnWriteEnvelopeSchema = z
|
|
|
95
96
|
current_phase: z.string(),
|
|
96
97
|
active: z.boolean(),
|
|
97
98
|
receipt: RequiredWorkflowStateReceiptSchema,
|
|
99
|
+
state_revision: z.number().optional(),
|
|
98
100
|
})
|
|
99
101
|
.passthrough();
|
|
100
102
|
|
|
@@ -11,6 +11,13 @@ import {
|
|
|
11
11
|
type WorkflowStateMutationOwner,
|
|
12
12
|
type WorkflowStateReceipt,
|
|
13
13
|
} from "../skill-state/workflow-state-contract";
|
|
14
|
+
import {
|
|
15
|
+
activeEntryPath as layoutActiveEntryPath,
|
|
16
|
+
activeSnapshotPath as layoutActiveSnapshotPath,
|
|
17
|
+
activeStateDir as layoutActiveStateDir,
|
|
18
|
+
auditPath as layoutAuditPath,
|
|
19
|
+
transactionJournalPath as layoutTransactionJournalPath,
|
|
20
|
+
} from "./session-layout";
|
|
14
21
|
import { RequiredOnWriteEnvelopeSchema } from "./state-schema";
|
|
15
22
|
|
|
16
23
|
/**
|
|
@@ -22,7 +29,7 @@ import { RequiredOnWriteEnvelopeSchema } from "./state-schema";
|
|
|
22
29
|
* supplied mutation context. No lockfiles are used; isolation is by atomic rename,
|
|
23
30
|
* append, O_EXCL creates, conditional deletes, per-entry active-state files,
|
|
24
31
|
* and derived active-state snapshots.
|
|
25
|
-
* Transaction journals are per mutation id under
|
|
32
|
+
* Transaction journals are per mutation id under the session state transactions directory;
|
|
26
33
|
* they are recovery evidence only, never global locks or waiters, so stale
|
|
27
34
|
* journals do not block unrelated state reads or writes.
|
|
28
35
|
*/
|
|
@@ -46,10 +53,15 @@ export interface StateWriterReceiptContext {
|
|
|
46
53
|
sessionId?: string;
|
|
47
54
|
mutationId?: string;
|
|
48
55
|
nowIso?: string;
|
|
56
|
+
verb?: string;
|
|
57
|
+
fromPhase?: string;
|
|
58
|
+
toPhase?: string;
|
|
59
|
+
forced?: boolean;
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
export interface StateWriterAuditContext {
|
|
52
63
|
cwd?: string;
|
|
64
|
+
sessionId?: string;
|
|
53
65
|
category: WriterCategory;
|
|
54
66
|
verb: string;
|
|
55
67
|
owner: WorkflowStateMutationOwner;
|
|
@@ -78,10 +90,23 @@ export interface WorkflowTransactionJournal {
|
|
|
78
90
|
steps: string[];
|
|
79
91
|
}
|
|
80
92
|
|
|
93
|
+
export type StateWritePolicy = "source" | "cache";
|
|
94
|
+
|
|
95
|
+
export interface GuardedStateWriterOptions extends StateWriterOptions {
|
|
96
|
+
policy: StateWritePolicy;
|
|
97
|
+
expectedRevision?: number;
|
|
98
|
+
sourceRevision?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type GuardedWriteResult =
|
|
102
|
+
| { path: string; written: true }
|
|
103
|
+
| { path: string; written: false; reason: "stale-skip" };
|
|
104
|
+
|
|
81
105
|
export interface StateWriterOptions {
|
|
82
106
|
cwd?: string;
|
|
83
107
|
receipt?: StateWriterReceiptContext;
|
|
84
108
|
audit?: StateWriterAuditContext;
|
|
109
|
+
sourceRevision?: number;
|
|
85
110
|
/**
|
|
86
111
|
* Cross-process lock tuning for read-modify-write paths that route through
|
|
87
112
|
* `withWorkflowStateLock` / `updateJsonAtomic`. Omit for the hardened
|
|
@@ -90,6 +115,19 @@ export interface StateWriterOptions {
|
|
|
90
115
|
lock?: FileLockOptions;
|
|
91
116
|
}
|
|
92
117
|
|
|
118
|
+
export class StateWriteConflictError extends Error {
|
|
119
|
+
constructor(
|
|
120
|
+
public readonly path: string,
|
|
121
|
+
public readonly expectedRevision: number,
|
|
122
|
+
public readonly persistedRevision: number,
|
|
123
|
+
) {
|
|
124
|
+
super(
|
|
125
|
+
`state write conflict at ${path}: expected revision ${expectedRevision}, persisted revision ${persistedRevision}`,
|
|
126
|
+
);
|
|
127
|
+
this.name = "StateWriteConflictError";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
93
131
|
export interface DeleteIfOwnedOptions extends StateWriterOptions {
|
|
94
132
|
predicate?: (current: unknown) => boolean | Promise<boolean>;
|
|
95
133
|
}
|
|
@@ -255,32 +293,23 @@ function safeString(value: unknown): string {
|
|
|
255
293
|
return typeof value === "string" ? value : "";
|
|
256
294
|
}
|
|
257
295
|
|
|
258
|
-
function
|
|
259
|
-
|
|
296
|
+
function requireSessionId(sessionScope: string | ActiveSessionScope | undefined, source: string): string {
|
|
297
|
+
const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
|
|
298
|
+
const normalizedSessionId = safeString(sessionId).trim();
|
|
299
|
+
if (!normalizedSessionId) throw new Error(`a non-empty GJC session id is required (${source})`);
|
|
300
|
+
return normalizedSessionId;
|
|
260
301
|
}
|
|
261
302
|
|
|
262
303
|
function activeStateDir(cwd: string, sessionScope?: string | ActiveSessionScope): string {
|
|
263
|
-
|
|
264
|
-
const normalizedSessionId = safeString(sessionId).trim();
|
|
265
|
-
const stateDir = path.join(cwd, ".gjc", "state");
|
|
266
|
-
return normalizedSessionId
|
|
267
|
-
? path.join(stateDir, "sessions", encodePathSegment(normalizedSessionId), "active")
|
|
268
|
-
: path.join(stateDir, "active");
|
|
304
|
+
return layoutActiveStateDir(cwd, requireSessionId(sessionScope, "activeStateDir"));
|
|
269
305
|
}
|
|
270
306
|
|
|
271
307
|
function activeSnapshotPath(cwd: string, sessionScope?: string | ActiveSessionScope): string {
|
|
272
|
-
|
|
273
|
-
const normalizedSessionId = safeString(sessionId).trim();
|
|
274
|
-
const stateDir = path.join(cwd, ".gjc", "state");
|
|
275
|
-
return normalizedSessionId
|
|
276
|
-
? path.join(stateDir, "sessions", encodePathSegment(normalizedSessionId), "skill-active-state.json")
|
|
277
|
-
: path.join(stateDir, "skill-active-state.json");
|
|
308
|
+
return layoutActiveSnapshotPath(cwd, requireSessionId(sessionScope, "activeSnapshotPath"));
|
|
278
309
|
}
|
|
279
310
|
|
|
280
311
|
function activeEntryPath(cwd: string, sessionScope: string | ActiveSessionScope | undefined, skill: string): string {
|
|
281
|
-
|
|
282
|
-
if (!normalizedSkill) throw new Error("skill is required");
|
|
283
|
-
return path.join(activeStateDir(cwd, sessionScope), `${encodePathSegment(normalizedSkill)}.json`);
|
|
312
|
+
return layoutActiveEntryPath(cwd, requireSessionId(sessionScope, "activeEntryPath"), skill);
|
|
284
313
|
}
|
|
285
314
|
|
|
286
315
|
function activeSubskillKey(entry: ActiveSubskillEntry): string {
|
|
@@ -356,14 +385,68 @@ async function readJsonIfPresent(filePath: string): Promise<unknown | undefined>
|
|
|
356
385
|
}
|
|
357
386
|
}
|
|
358
387
|
|
|
388
|
+
// Corrupt-tolerant variant for the guarded writers' revision computation: a prior
|
|
389
|
+
// file that is unparseable has no usable revision, so treat it as absent (revision 0)
|
|
390
|
+
// rather than throwing. This lets an authoritative/forced write overwrite corrupt
|
|
391
|
+
// state and a derived cache write overwrite (not stale-skip) corrupt cache.
|
|
392
|
+
async function readJsonIfPresentTolerant(filePath: string): Promise<unknown | undefined> {
|
|
393
|
+
try {
|
|
394
|
+
return await readJsonIfPresent(filePath);
|
|
395
|
+
} catch {
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function persistedStateRevision(value: unknown): number {
|
|
401
|
+
if (!isPlainObject(value)) return 0;
|
|
402
|
+
const revision = value.state_revision;
|
|
403
|
+
return typeof revision === "number" && Number.isFinite(revision) ? revision : 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function persistedSourceRevision(value: unknown): number {
|
|
407
|
+
if (!isPlainObject(value)) return 0;
|
|
408
|
+
const revision = value.source_state_revision;
|
|
409
|
+
return typeof revision === "number" && Number.isFinite(revision) ? revision : persistedStateRevision(value);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function withoutCandidateRevision(value: unknown): unknown {
|
|
413
|
+
if (!isPlainObject(value)) return value;
|
|
414
|
+
const next = { ...value };
|
|
415
|
+
delete next.state_revision;
|
|
416
|
+
return next;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function stampStateRevision(value: unknown, stateRevision: number, sourceRevision?: number): unknown {
|
|
420
|
+
if (!isPlainObject(value)) return value;
|
|
421
|
+
const next = withoutCandidateRevision(value) as Record<string, unknown>;
|
|
422
|
+
return {
|
|
423
|
+
...next,
|
|
424
|
+
...(sourceRevision === undefined ? {} : { source_state_revision: sourceRevision }),
|
|
425
|
+
state_revision: stateRevision,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
359
429
|
function withWorkflowReceipt(value: unknown, receipt: WorkflowStateReceipt | undefined): unknown {
|
|
360
430
|
if (!receipt || !value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
361
431
|
return { ...(value as Record<string, unknown>), receipt };
|
|
362
432
|
}
|
|
363
433
|
|
|
434
|
+
function stampWorkflowEnvelopeRevisionAndChecksum(
|
|
435
|
+
value: unknown,
|
|
436
|
+
filePath: string,
|
|
437
|
+
stateRevision: number,
|
|
438
|
+
sourceRevision: number | undefined,
|
|
439
|
+
options: StateWriterOptions | undefined,
|
|
440
|
+
): unknown {
|
|
441
|
+
return stampWorkflowEnvelopeChecksum(
|
|
442
|
+
stampStateRevision(withWorkflowReceipt(value, buildReceipt(options)), stateRevision, sourceRevision),
|
|
443
|
+
filePath,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
364
447
|
function buildReceipt(options: StateWriterOptions | undefined): WorkflowStateReceipt | undefined {
|
|
365
448
|
if (!options?.receipt) return undefined;
|
|
366
|
-
|
|
449
|
+
const receipt = buildWorkflowStateReceipt({
|
|
367
450
|
cwd: path.resolve(options.receipt.cwd ?? options.cwd ?? process.cwd()),
|
|
368
451
|
skill: options.receipt.skill,
|
|
369
452
|
owner: options.receipt.owner,
|
|
@@ -372,13 +455,18 @@ function buildReceipt(options: StateWriterOptions | undefined): WorkflowStateRec
|
|
|
372
455
|
nowIso: options.receipt.nowIso,
|
|
373
456
|
mutationId: options.receipt.mutationId,
|
|
374
457
|
});
|
|
458
|
+
receipt.verb = options.receipt.verb;
|
|
459
|
+
receipt.from_phase = options.receipt.fromPhase;
|
|
460
|
+
receipt.to_phase = options.receipt.toPhase;
|
|
461
|
+
receipt.forced = options.receipt.forced;
|
|
462
|
+
return receipt;
|
|
375
463
|
}
|
|
376
464
|
|
|
377
465
|
async function maybeAudit(mutatedPath: string, options?: StateWriterOptions): Promise<void> {
|
|
378
466
|
if (!options?.audit) return;
|
|
379
467
|
const audit = options.audit;
|
|
380
468
|
const cwd = path.resolve(audit.cwd ?? options.cwd ?? process.cwd());
|
|
381
|
-
await appendAuditEntry(cwd, {
|
|
469
|
+
await appendAuditEntry(cwd, options?.audit?.sessionId ?? "", {
|
|
382
470
|
ts: new Date().toISOString(),
|
|
383
471
|
skill: audit.skill,
|
|
384
472
|
category: audit.category,
|
|
@@ -405,6 +493,118 @@ async function atomicWrite(filePath: string, content: string): Promise<string> {
|
|
|
405
493
|
return filePath;
|
|
406
494
|
}
|
|
407
495
|
|
|
496
|
+
async function writeGuardedResolvedJsonAtomic(
|
|
497
|
+
filePath: string,
|
|
498
|
+
value: unknown,
|
|
499
|
+
options: GuardedStateWriterOptions,
|
|
500
|
+
): Promise<GuardedWriteResult> {
|
|
501
|
+
return lockResolvedWorkflowTarget(
|
|
502
|
+
filePath,
|
|
503
|
+
async () => {
|
|
504
|
+
const current = await readJsonIfPresentTolerant(filePath);
|
|
505
|
+
const currentRevision = persistedStateRevision(current);
|
|
506
|
+
|
|
507
|
+
if (options.policy === "source") {
|
|
508
|
+
if (options.expectedRevision !== undefined && options.expectedRevision !== currentRevision) {
|
|
509
|
+
throw new StateWriteConflictError(filePath, options.expectedRevision, currentRevision);
|
|
510
|
+
}
|
|
511
|
+
const next = stampStateRevision(withWorkflowReceipt(value, buildReceipt(options)), currentRevision + 1);
|
|
512
|
+
await atomicWrite(filePath, jsonText(next));
|
|
513
|
+
await maybeAudit(filePath, options);
|
|
514
|
+
return { path: filePath, written: true };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const incomingSourceRevision =
|
|
518
|
+
options.sourceRevision ?? (isPlainObject(value) ? persistedStateRevision(value) : 0);
|
|
519
|
+
if (current !== undefined && incomingSourceRevision <= persistedSourceRevision(current)) {
|
|
520
|
+
return { path: filePath, written: false, reason: "stale-skip" };
|
|
521
|
+
}
|
|
522
|
+
const next = stampStateRevision(
|
|
523
|
+
withWorkflowReceipt(value, buildReceipt(options)),
|
|
524
|
+
currentRevision + 1,
|
|
525
|
+
incomingSourceRevision,
|
|
526
|
+
);
|
|
527
|
+
await atomicWrite(filePath, jsonText(next));
|
|
528
|
+
await maybeAudit(filePath, options);
|
|
529
|
+
return { path: filePath, written: true };
|
|
530
|
+
},
|
|
531
|
+
options.lock,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export async function writeGuardedJsonAtomic(
|
|
536
|
+
targetPath: string,
|
|
537
|
+
value: unknown,
|
|
538
|
+
options: GuardedStateWriterOptions,
|
|
539
|
+
): Promise<GuardedWriteResult> {
|
|
540
|
+
const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
|
|
541
|
+
return writeGuardedResolvedJsonAtomic(filePath, value, options);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export async function writeGuardedWorkflowEnvelopeAtomic(
|
|
545
|
+
targetPath: string,
|
|
546
|
+
value: unknown,
|
|
547
|
+
options: GuardedStateWriterOptions,
|
|
548
|
+
): Promise<GuardedWriteResult> {
|
|
549
|
+
const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
|
|
550
|
+
return lockResolvedWorkflowTarget(
|
|
551
|
+
filePath,
|
|
552
|
+
async () => {
|
|
553
|
+
const current = await readJsonIfPresentTolerant(filePath);
|
|
554
|
+
const currentRevision = persistedStateRevision(current);
|
|
555
|
+
|
|
556
|
+
if (options.policy === "source") {
|
|
557
|
+
if (options.expectedRevision !== undefined && options.expectedRevision !== currentRevision) {
|
|
558
|
+
throw new StateWriteConflictError(filePath, options.expectedRevision, currentRevision);
|
|
559
|
+
}
|
|
560
|
+
const next = stampWorkflowEnvelopeRevisionAndChecksum(
|
|
561
|
+
value,
|
|
562
|
+
filePath,
|
|
563
|
+
currentRevision + 1,
|
|
564
|
+
undefined,
|
|
565
|
+
options,
|
|
566
|
+
);
|
|
567
|
+
const parsed = RequiredOnWriteEnvelopeSchema.safeParse(next);
|
|
568
|
+
if (!parsed.success) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
`Refusing to write invalid workflow state envelope to ${filePath}: ${parsed.error.issues
|
|
571
|
+
.map(issue => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
|
|
572
|
+
.join("; ")}`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
await atomicWrite(filePath, jsonText(next));
|
|
576
|
+
await maybeAudit(filePath, options);
|
|
577
|
+
return { path: filePath, written: true };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const incomingSourceRevision =
|
|
581
|
+
options.sourceRevision ?? (isPlainObject(value) ? persistedStateRevision(value) : 0);
|
|
582
|
+
if (current !== undefined && incomingSourceRevision <= persistedSourceRevision(current)) {
|
|
583
|
+
return { path: filePath, written: false, reason: "stale-skip" };
|
|
584
|
+
}
|
|
585
|
+
const next = stampWorkflowEnvelopeRevisionAndChecksum(
|
|
586
|
+
value,
|
|
587
|
+
filePath,
|
|
588
|
+
currentRevision + 1,
|
|
589
|
+
incomingSourceRevision,
|
|
590
|
+
options,
|
|
591
|
+
);
|
|
592
|
+
const parsed = RequiredOnWriteEnvelopeSchema.safeParse(next);
|
|
593
|
+
if (!parsed.success) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`Refusing to write invalid workflow state envelope to ${filePath}: ${parsed.error.issues
|
|
596
|
+
.map(issue => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
|
|
597
|
+
.join("; ")}`,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
await atomicWrite(filePath, jsonText(next));
|
|
601
|
+
await maybeAudit(filePath, options);
|
|
602
|
+
return { path: filePath, written: true };
|
|
603
|
+
},
|
|
604
|
+
options.lock,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
408
608
|
export async function writeJsonAtomic(
|
|
409
609
|
targetPath: string,
|
|
410
610
|
value: unknown,
|
|
@@ -449,7 +649,7 @@ async function recordInvalidWorkflowTransition(args: {
|
|
|
449
649
|
// internal write skipped a manifest edge.
|
|
450
650
|
const cwd = path.resolve(options?.audit?.cwd ?? options?.cwd ?? process.cwd());
|
|
451
651
|
try {
|
|
452
|
-
await appendAuditEntry(cwd, {
|
|
652
|
+
await appendAuditEntry(cwd, options?.audit?.sessionId ?? "", {
|
|
453
653
|
ts: new Date().toISOString(),
|
|
454
654
|
skill,
|
|
455
655
|
category: "state",
|
|
@@ -756,8 +956,7 @@ export async function removeFileAudited(targetPath: string, options?: StateWrite
|
|
|
756
956
|
}
|
|
757
957
|
|
|
758
958
|
/**
|
|
759
|
-
* Active entry files under `.gjc/state/active/<skill>.json`
|
|
760
|
-
* `.gjc/state/sessions/<id>/active/<skill>.json` are authoritative. The
|
|
959
|
+
* Active entry files under `.gjc/_session-{id}/state/active/<skill>.json` are authoritative. The
|
|
761
960
|
* adjacent `skill-active-state.json` file is only a derived cache rebuilt from
|
|
762
961
|
* those entries, so concurrent snapshot rebuilds can race without losing any
|
|
763
962
|
* writer's per-skill state.
|
|
@@ -770,8 +969,16 @@ export async function writeActiveEntry(
|
|
|
770
969
|
options?: StateWriterOptions,
|
|
771
970
|
): Promise<string> {
|
|
772
971
|
const filePath = activeEntryPath(path.resolve(cwd), sessionScope, skill);
|
|
773
|
-
await
|
|
774
|
-
|
|
972
|
+
await writeGuardedResolvedJsonAtomic(
|
|
973
|
+
filePath,
|
|
974
|
+
{ ...entry, skill },
|
|
975
|
+
{
|
|
976
|
+
...options,
|
|
977
|
+
policy: "cache",
|
|
978
|
+
sourceRevision:
|
|
979
|
+
persistedSourceRevision(entry) || persistedSourceRevision(await readJsonIfPresent(filePath)) + 1,
|
|
980
|
+
},
|
|
981
|
+
);
|
|
775
982
|
return filePath;
|
|
776
983
|
}
|
|
777
984
|
|
|
@@ -782,9 +989,24 @@ export async function removeActiveEntry(
|
|
|
782
989
|
options?: StateWriterOptions,
|
|
783
990
|
): Promise<DeleteResult> {
|
|
784
991
|
const filePath = activeEntryPath(path.resolve(cwd), sessionScope, skill);
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
992
|
+
return lockResolvedWorkflowTarget(
|
|
993
|
+
filePath,
|
|
994
|
+
async () => {
|
|
995
|
+
const current = await readJsonIfPresent(filePath);
|
|
996
|
+
const incomingSourceRevision = options?.sourceRevision;
|
|
997
|
+
if (
|
|
998
|
+
current !== undefined &&
|
|
999
|
+
incomingSourceRevision !== undefined &&
|
|
1000
|
+
incomingSourceRevision < persistedSourceRevision(current)
|
|
1001
|
+
) {
|
|
1002
|
+
return { path: filePath, deleted: false };
|
|
1003
|
+
}
|
|
1004
|
+
const deleted = await atomicRemove(filePath);
|
|
1005
|
+
if (deleted) await maybeAudit(filePath, options);
|
|
1006
|
+
return { path: filePath, deleted };
|
|
1007
|
+
},
|
|
1008
|
+
options?.lock,
|
|
1009
|
+
);
|
|
788
1010
|
}
|
|
789
1011
|
|
|
790
1012
|
export async function readActiveEntries(
|
|
@@ -819,8 +1041,14 @@ export async function rebuildActiveSnapshot(
|
|
|
819
1041
|
const resolvedCwd = path.resolve(cwd);
|
|
820
1042
|
const snapshotPath = activeSnapshotPath(resolvedCwd, sessionScope);
|
|
821
1043
|
const entries = await readActiveEntries(resolvedCwd, sessionScope);
|
|
822
|
-
await
|
|
823
|
-
|
|
1044
|
+
await writeGuardedResolvedJsonAtomic(snapshotPath, buildActiveSnapshot(entries), {
|
|
1045
|
+
...options,
|
|
1046
|
+
policy: "cache",
|
|
1047
|
+
sourceRevision: Math.max(
|
|
1048
|
+
persistedSourceRevision(await readJsonIfPresent(snapshotPath)) + 1,
|
|
1049
|
+
...entries.map(entry => persistedSourceRevision(entry)),
|
|
1050
|
+
),
|
|
1051
|
+
});
|
|
824
1052
|
return snapshotPath;
|
|
825
1053
|
}
|
|
826
1054
|
|
|
@@ -925,7 +1153,7 @@ export async function hardPrune(
|
|
|
925
1153
|
}
|
|
926
1154
|
if (options?.audit && removed.length > 0) {
|
|
927
1155
|
const audit = options.audit;
|
|
928
|
-
await appendAuditEntry(path.resolve(audit.cwd ?? options.cwd ?? process.cwd()), {
|
|
1156
|
+
await appendAuditEntry(path.resolve(audit.cwd ?? options.cwd ?? process.cwd()), audit.sessionId ?? "", {
|
|
929
1157
|
ts: new Date().toISOString(),
|
|
930
1158
|
skill: audit.skill,
|
|
931
1159
|
category: audit.category,
|
|
@@ -967,26 +1195,41 @@ export async function forceOverwrite(
|
|
|
967
1195
|
);
|
|
968
1196
|
}
|
|
969
1197
|
|
|
970
|
-
export async function appendAuditEntry(
|
|
971
|
-
|
|
1198
|
+
export async function appendAuditEntry(
|
|
1199
|
+
cwd: string,
|
|
1200
|
+
sessionIdOrEntry: string | AuditEntry,
|
|
1201
|
+
maybeEntry?: AuditEntry,
|
|
1202
|
+
): Promise<string> {
|
|
1203
|
+
const sessionId =
|
|
1204
|
+
typeof sessionIdOrEntry === "string"
|
|
1205
|
+
? sessionIdOrEntry.trim()
|
|
1206
|
+
: safeString((sessionIdOrEntry as AuditEntry & { session_id?: unknown }).session_id).trim();
|
|
1207
|
+
if (!sessionId) throw new Error("a non-empty GJC session id is required (appendAuditEntry)");
|
|
1208
|
+
const entry = typeof sessionIdOrEntry === "string" ? maybeEntry : sessionIdOrEntry;
|
|
1209
|
+
if (!entry) throw new Error("audit entry is required");
|
|
1210
|
+
const filePath = resolveGjcTarget(layoutAuditPath(cwd, sessionId), cwd);
|
|
972
1211
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
973
1212
|
await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf-8");
|
|
974
1213
|
return filePath;
|
|
975
1214
|
}
|
|
976
1215
|
|
|
977
|
-
function transactionJournalPath(cwd: string, mutationId: string): string {
|
|
978
|
-
return
|
|
1216
|
+
function transactionJournalPath(cwd: string, sessionId: string, mutationId: string): string {
|
|
1217
|
+
return layoutTransactionJournalPath(path.resolve(cwd), sessionId, mutationId);
|
|
979
1218
|
}
|
|
980
1219
|
|
|
981
1220
|
export async function readWorkflowTransactionJournal(
|
|
982
1221
|
cwd: string,
|
|
1222
|
+
sessionId: string,
|
|
983
1223
|
mutationId: string,
|
|
984
1224
|
): Promise<WorkflowTransactionJournal | undefined> {
|
|
985
|
-
return (await readJsonIfPresent(transactionJournalPath(cwd, mutationId))) as
|
|
1225
|
+
return (await readJsonIfPresent(transactionJournalPath(cwd, sessionId, mutationId))) as
|
|
1226
|
+
| WorkflowTransactionJournal
|
|
1227
|
+
| undefined;
|
|
986
1228
|
}
|
|
987
1229
|
|
|
988
1230
|
export async function beginWorkflowTransactionJournal(input: {
|
|
989
1231
|
cwd: string;
|
|
1232
|
+
sessionId: string;
|
|
990
1233
|
mutationId: string;
|
|
991
1234
|
caller?: CanonicalGjcWorkflowSkill;
|
|
992
1235
|
callee?: CanonicalGjcWorkflowSkill;
|
|
@@ -1005,7 +1248,7 @@ export async function beginWorkflowTransactionJournal(input: {
|
|
|
1005
1248
|
steps: [],
|
|
1006
1249
|
};
|
|
1007
1250
|
try {
|
|
1008
|
-
return await createJsonNoClobber(transactionJournalPath(input.cwd, input.mutationId), journal, {
|
|
1251
|
+
return await createJsonNoClobber(transactionJournalPath(input.cwd, input.sessionId, input.mutationId), journal, {
|
|
1009
1252
|
cwd: input.cwd,
|
|
1010
1253
|
});
|
|
1011
1254
|
} catch (error) {
|
|
@@ -1016,17 +1259,22 @@ export async function beginWorkflowTransactionJournal(input: {
|
|
|
1016
1259
|
|
|
1017
1260
|
export async function updateWorkflowTransactionJournal(
|
|
1018
1261
|
cwd: string,
|
|
1262
|
+
sessionId: string,
|
|
1019
1263
|
mutationId: string,
|
|
1020
1264
|
patch: Partial<WorkflowTransactionJournal>,
|
|
1021
1265
|
): Promise<string> {
|
|
1022
|
-
const filePath = transactionJournalPath(cwd, mutationId);
|
|
1266
|
+
const filePath = transactionJournalPath(cwd, sessionId, mutationId);
|
|
1023
1267
|
const current = ((await readJsonIfPresent(filePath)) ?? {}) as WorkflowTransactionJournal;
|
|
1024
1268
|
const next = { ...current, ...patch, updated_at: new Date().toISOString() } as WorkflowTransactionJournal;
|
|
1025
1269
|
await atomicWrite(filePath, jsonText(next));
|
|
1026
1270
|
return filePath;
|
|
1027
1271
|
}
|
|
1028
1272
|
|
|
1029
|
-
export async function completeWorkflowTransactionJournal(
|
|
1030
|
-
|
|
1031
|
-
|
|
1273
|
+
export async function completeWorkflowTransactionJournal(
|
|
1274
|
+
cwd: string,
|
|
1275
|
+
sessionId: string,
|
|
1276
|
+
mutationId: string,
|
|
1277
|
+
): Promise<void> {
|
|
1278
|
+
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, { status: "committed" });
|
|
1279
|
+
await atomicRemove(transactionJournalPath(cwd, sessionId, mutationId)).catch(() => false);
|
|
1032
1280
|
}
|
|
@@ -5,8 +5,9 @@ import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
|
5
5
|
import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
|
|
6
6
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
7
7
|
import type { GcPidProbe, GcRecord } from "./gc-runtime";
|
|
8
|
-
|
|
9
8
|
import { applyGjcTmuxProfile, GJC_TMUX_LAUNCHED_ENV } from "./launch-tmux";
|
|
9
|
+
import { modeStatePath, sessionIdFromDirName, sessionReportsDir, teamStateRoot } from "./session-layout";
|
|
10
|
+
import { resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
10
11
|
import {
|
|
11
12
|
AlreadyExistsError,
|
|
12
13
|
appendJsonl as appendJsonlAudited,
|
|
@@ -554,7 +555,14 @@ function stateWriterOptions(filePath: string, category: "state" | "ledger" | "re
|
|
|
554
555
|
const marker = `${path.sep}.gjc${path.sep}`;
|
|
555
556
|
const markerIndex = resolved.indexOf(marker);
|
|
556
557
|
const cwd = markerIndex >= 0 ? resolved.slice(0, markerIndex) : process.cwd();
|
|
557
|
-
|
|
558
|
+
const parts = resolved.split(path.sep);
|
|
559
|
+
const sessionId =
|
|
560
|
+
parts.map(part => sessionIdFromDirName(part)).find((value): value is string => Boolean(value)) ??
|
|
561
|
+
process.env.GJC_SESSION_ID?.trim();
|
|
562
|
+
// Session-scoped audit requires a GJC session. When an explicit env-root override
|
|
563
|
+
// (e.g. GJC_TEAM_STATE_ROOT) is in effect with no resolvable session, omit the audit
|
|
564
|
+
// context entirely so the override write does not fail on a session-scoped audit.
|
|
565
|
+
return sessionId ? { cwd, audit: { category, verb, owner: "gjc-runtime" as const, sessionId } } : { cwd };
|
|
558
566
|
}
|
|
559
567
|
|
|
560
568
|
function sanitizeName(value: string): string {
|
|
@@ -668,7 +676,8 @@ function workerIntegrationDedupePath(dir: string, worker: string): string {
|
|
|
668
676
|
export function resolveGjcTeamStateRoot(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
|
|
669
677
|
const explicit = env.GJC_TEAM_STATE_ROOT?.trim();
|
|
670
678
|
if (explicit) return path.resolve(cwd, explicit);
|
|
671
|
-
|
|
679
|
+
const session = resolveGjcSessionForWrite(cwd, { envSessionId: env.GJC_SESSION_ID });
|
|
680
|
+
return teamStateRoot(cwd, session.gjcSessionId);
|
|
672
681
|
}
|
|
673
682
|
|
|
674
683
|
async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
@@ -1170,15 +1179,17 @@ async function writeWorkerLifecycleForConfig(
|
|
|
1170
1179
|
return Object.fromEntries(entries.map(entry => [entry.worker, entry]));
|
|
1171
1180
|
}
|
|
1172
1181
|
|
|
1173
|
-
function teamModeStatePath(): string {
|
|
1174
|
-
return
|
|
1182
|
+
function teamModeStatePath(cwd: string, sessionId: string): string {
|
|
1183
|
+
return modeStatePath(cwd, sessionId, "team");
|
|
1175
1184
|
}
|
|
1176
1185
|
|
|
1177
1186
|
export async function persistGjcTeamModeStateSummary(snapshot: GjcTeamSnapshot, cwd = process.cwd()): Promise<void> {
|
|
1178
1187
|
const active = snapshot.phase !== "complete" && snapshot.phase !== "cancelled";
|
|
1179
1188
|
const updatedAt = now();
|
|
1189
|
+
const sessionId = resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
|
|
1190
|
+
const statePath = teamModeStatePath(cwd, sessionId);
|
|
1180
1191
|
await writeWorkflowEnvelopeAtomic(
|
|
1181
|
-
|
|
1192
|
+
statePath,
|
|
1182
1193
|
{
|
|
1183
1194
|
skill: "team",
|
|
1184
1195
|
version: WORKFLOW_STATE_VERSION,
|
|
@@ -1195,11 +1206,13 @@ export async function persistGjcTeamModeStateSummary(snapshot: GjcTeamSnapshot,
|
|
|
1195
1206
|
skill: "team",
|
|
1196
1207
|
owner: "gjc-runtime",
|
|
1197
1208
|
command: "gjc team sync-team-summary",
|
|
1209
|
+
sessionId,
|
|
1198
1210
|
nowIso: updatedAt,
|
|
1199
1211
|
},
|
|
1200
|
-
audit: { category: "state", verb: "sync-team-summary", owner: "gjc-runtime", skill: "team" },
|
|
1212
|
+
audit: { category: "state", verb: "sync-team-summary", owner: "gjc-runtime", skill: "team", sessionId },
|
|
1201
1213
|
},
|
|
1202
1214
|
);
|
|
1215
|
+
await writeSessionActivityMarker(cwd, sessionId, { writer: "team-runtime", path: statePath });
|
|
1203
1216
|
}
|
|
1204
1217
|
|
|
1205
1218
|
function appendLivenessRecoveryReason(
|
|
@@ -2120,7 +2133,14 @@ function integrationReportPath(dir: string): string {
|
|
|
2120
2133
|
return path.join(dir, "integration-report.md");
|
|
2121
2134
|
}
|
|
2122
2135
|
function commitHygieneLedgerPath(config: GjcTeamConfig): string {
|
|
2123
|
-
return path.join(
|
|
2136
|
+
return path.join(
|
|
2137
|
+
sessionReportsDir(
|
|
2138
|
+
config.leader_cwd,
|
|
2139
|
+
resolveGjcSessionForWrite(config.leader_cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId,
|
|
2140
|
+
),
|
|
2141
|
+
"team-commit-hygiene",
|
|
2142
|
+
`${config.team_name}.ledger.json`,
|
|
2143
|
+
);
|
|
2124
2144
|
}
|
|
2125
2145
|
function integrationNowState(
|
|
2126
2146
|
status: GjcTeamIntegrationStatus,
|
|
@@ -2186,11 +2206,15 @@ export type GjcWorkerCheckpointClassification =
|
|
|
2186
2206
|
|
|
2187
2207
|
const UNMERGED_GIT_STATUS_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
|
|
2188
2208
|
const PROTECTED_WORKER_CHECKPOINT_PREFIXES = [
|
|
2189
|
-
".gjc/state/",
|
|
2190
|
-
".gjc/logs/",
|
|
2191
|
-
".gjc/reports/",
|
|
2192
|
-
".gjc/
|
|
2193
|
-
".gjc/ultragoal/",
|
|
2209
|
+
".gjc/_session-*/state/",
|
|
2210
|
+
".gjc/_session-*/logs/",
|
|
2211
|
+
".gjc/_session-*/reports/",
|
|
2212
|
+
".gjc/_session-*/runtime/",
|
|
2213
|
+
".gjc/_session-*/ultragoal/",
|
|
2214
|
+
".gjc/_session-*/plans/",
|
|
2215
|
+
".gjc/_session-*/specs/",
|
|
2216
|
+
".gjc/_session-*/rlm/",
|
|
2217
|
+
".gjc/_session-*/audit/",
|
|
2194
2218
|
];
|
|
2195
2219
|
|
|
2196
2220
|
function parsePorcelainStatusFiles(stdout: string): string[] {
|
|
@@ -2211,12 +2235,12 @@ export function classifyGjcTeamCheckpointFiles(files: string[]): { eligible: str
|
|
|
2211
2235
|
const protectedFiles: string[] = [];
|
|
2212
2236
|
for (const file of files) {
|
|
2213
2237
|
const normalized = normalizeGitStatusPath(file);
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
)
|
|
2218
|
-
)
|
|
2219
|
-
|
|
2238
|
+
const isProtected = PROTECTED_WORKER_CHECKPOINT_PREFIXES.some(prefix => {
|
|
2239
|
+
if (!prefix.includes("*")) return normalized === prefix.slice(0, -1) || normalized.startsWith(prefix);
|
|
2240
|
+
const [head, tail] = prefix.split("*");
|
|
2241
|
+
return Boolean(head && tail) && normalized.startsWith(head) && normalized.slice(head.length).includes(tail);
|
|
2242
|
+
});
|
|
2243
|
+
if (isProtected) protectedFiles.push(file);
|
|
2220
2244
|
else eligible.push(file);
|
|
2221
2245
|
}
|
|
2222
2246
|
return { eligible, protected: protectedFiles };
|
|
@@ -114,7 +114,13 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
|
|
|
114
114
|
output = runTmux(["list-sessions", "-F", format], env);
|
|
115
115
|
} catch (error) {
|
|
116
116
|
const message = error instanceof Error ? error.message : String(error);
|
|
117
|
-
if (
|
|
117
|
+
if (
|
|
118
|
+
message.includes("no server running") ||
|
|
119
|
+
message.includes("failed to connect to server") ||
|
|
120
|
+
message.includes("error connecting to")
|
|
121
|
+
) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
118
124
|
throw error;
|
|
119
125
|
}
|
|
120
126
|
return output
|