@sayknow-cli/coding-agent 0.2.3 → 0.2.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 +15 -0
- package/dist/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/cli/update-cli.d.ts +3 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/config/keybindings.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +27 -0
- package/dist/types/harness-control-plane/storage.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +12 -4
- package/dist/types/i18n/messages/en.d.ts +14 -0
- package/dist/types/lsp/startup-events.d.ts +1 -0
- 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/components/welcome.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- 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/skc-runtime/deep-interview-recorder.d.ts +2 -0
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +2 -2
- package/dist/types/skc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/skc-runtime/session-layout.d.ts +59 -0
- package/dist/types/skc-runtime/session-resolution.d.ts +47 -0
- package/dist/types/skc-runtime/state-graph.d.ts +1 -1
- package/dist/types/skc-runtime/state-runtime.d.ts +5 -4
- package/dist/types/skc-runtime/state-schema.d.ts +2 -0
- package/dist/types/skc-runtime/state-writer.d.ts +36 -7
- package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +7 -4
- package/dist/types/skc-runtime/workflow-command-ref.d.ts +1 -1
- package/dist/types/skc-runtime/workflow-manifest.d.ts +1 -1
- 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/deep-interview-mutation-guard.d.ts +5 -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/scripts/build-binary.ts +0 -7
- package/src/cli/migrate-cli.ts +106 -0
- package/src/cli/setup-cli.ts +14 -1
- package/src/cli/update-cli.ts +53 -3
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +2 -2
- package/src/commands/launch.ts +1 -1
- package/src/commands/migrate.ts +46 -0
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +7 -3
- package/src/config/model-registry.ts +9 -2
- package/src/config/model-resolver.ts +13 -2
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +17 -0
- package/src/coordinator-mcp/policy.ts +10 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +0 -1
- package/src/defaults/skc/skills/deep-interview/SKILL.md +30 -24
- package/src/defaults/skc/skills/ralplan/SKILL.md +10 -4
- package/src/defaults/skc/skills/team/SKILL.md +51 -47
- package/src/defaults/skc/skills/ultragoal/SKILL.md +17 -13
- package/src/exec/bash-executor.ts +3 -1
- package/src/extensibility/custom-commands/loader.ts +0 -7
- package/src/extensibility/skc-plugins/injection.ts +23 -4
- package/src/extensibility/skc-plugins/state.ts +16 -1
- 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/i18n/messages/de.ts +14 -0
- package/src/i18n/messages/en.ts +14 -0
- package/src/i18n/messages/es.ts +14 -0
- package/src/i18n/messages/fr.ts +14 -0
- package/src/i18n/messages/ja.ts +14 -0
- package/src/i18n/messages/ko.ts +14 -0
- package/src/i18n/messages/zh.ts +14 -0
- package/src/internal-urls/docs-index.generated.ts +11 -9
- package/src/lsp/startup-events.ts +24 -0
- 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/components/model-selector.ts +42 -18
- package/src/modes/components/welcome.ts +20 -9
- package/src/modes/controllers/input-controller.ts +21 -3
- package/src/modes/interactive-mode.ts +27 -19
- package/src/modes/prompt-action-autocomplete.ts +11 -1
- 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 +43 -41
- package/src/session/session-manager.ts +19 -2
- package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
- package/src/setup/hermes-setup.ts +1 -1
- package/src/skc-runtime/deep-interview-recorder.ts +51 -18
- package/src/skc-runtime/deep-interview-runtime.ts +49 -23
- package/src/skc-runtime/goal-mode-request.ts +26 -11
- package/src/skc-runtime/launch-tmux.ts +68 -15
- package/src/skc-runtime/ralplan-runtime.ts +79 -50
- package/src/skc-runtime/session-layout.ts +180 -0
- package/src/skc-runtime/session-resolution.ts +217 -0
- package/src/skc-runtime/state-graph.ts +1 -2
- package/src/skc-runtime/state-migrations.ts +1 -0
- package/src/skc-runtime/state-runtime.ts +237 -114
- package/src/skc-runtime/state-schema.ts +2 -0
- package/src/skc-runtime/state-writer.ts +310 -42
- package/src/skc-runtime/team-runtime.ts +43 -19
- package/src/skc-runtime/tmux-sessions.ts +43 -2
- package/src/skc-runtime/ultragoal-guard.ts +45 -2
- package/src/skc-runtime/ultragoal-runtime.ts +121 -41
- package/src/skc-runtime/workflow-command-ref.ts +1 -2
- package/src/skc-runtime/workflow-manifest.ts +1 -2
- package/src/skill-state/active-state.ts +116 -129
- package/src/skill-state/canonical-skills.ts +4 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +238 -111
- package/src/skill-state/workflow-hud.ts +4 -2
- package/src/skill-state/workflow-state-contract.ts +3 -3
- package/src/slash-commands/builtin-registry.ts +8 -4
- package/src/system-prompt.ts +11 -9
- 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/ast-edit.ts +2 -2
- package/src/tools/computer.ts +58 -4
- package/src/utils/edit-mode.ts +1 -1
- 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 SKC 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, ".skc", "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, ".skc", "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 {
|
|
@@ -298,8 +327,28 @@ function flattenActiveSubskills(entries: SkillActiveEntry[]): ActiveSubskillEntr
|
|
|
298
327
|
return [...deduped.values()];
|
|
299
328
|
}
|
|
300
329
|
|
|
330
|
+
const CANONICAL_PIPELINE_RANK = new Map<string, number>([
|
|
331
|
+
["deep-interview", 0],
|
|
332
|
+
["ralplan", 1],
|
|
333
|
+
["ultragoal", 2],
|
|
334
|
+
]);
|
|
335
|
+
|
|
336
|
+
function canonicalPipelineRank(skill: string): number | undefined {
|
|
337
|
+
return CANONICAL_PIPELINE_RANK.get(skill);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function compareActiveEntryPrimary(a: SkillActiveEntry, b: SkillActiveEntry): number {
|
|
341
|
+
const aRank = canonicalPipelineRank(a.skill);
|
|
342
|
+
const bRank = canonicalPipelineRank(b.skill);
|
|
343
|
+
if (aRank !== undefined || bRank !== undefined) return (bRank ?? -1) - (aRank ?? -1);
|
|
344
|
+
const aTime = Date.parse(safeString(a.updated_at));
|
|
345
|
+
const bTime = Date.parse(safeString(b.updated_at));
|
|
346
|
+
if (Number.isFinite(aTime) || Number.isFinite(bTime)) return (bTime || 0) - (aTime || 0);
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
|
|
301
350
|
function buildActiveSnapshot(entries: SkillActiveEntry[]): SkillActiveState {
|
|
302
|
-
const visible = entries.filter(entry => entry.active !== false);
|
|
351
|
+
const visible = entries.filter(entry => entry.active !== false).toSorted(compareActiveEntryPrimary);
|
|
303
352
|
const primary = visible[0];
|
|
304
353
|
return {
|
|
305
354
|
version: 1,
|
|
@@ -336,14 +385,68 @@ async function readJsonIfPresent(filePath: string): Promise<unknown | undefined>
|
|
|
336
385
|
}
|
|
337
386
|
}
|
|
338
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
|
+
|
|
339
429
|
function withWorkflowReceipt(value: unknown, receipt: WorkflowStateReceipt | undefined): unknown {
|
|
340
430
|
if (!receipt || !value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
341
431
|
return { ...(value as Record<string, unknown>), receipt };
|
|
342
432
|
}
|
|
343
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
|
+
|
|
344
447
|
function buildReceipt(options: StateWriterOptions | undefined): WorkflowStateReceipt | undefined {
|
|
345
448
|
if (!options?.receipt) return undefined;
|
|
346
|
-
|
|
449
|
+
const receipt = buildWorkflowStateReceipt({
|
|
347
450
|
cwd: path.resolve(options.receipt.cwd ?? options.cwd ?? process.cwd()),
|
|
348
451
|
skill: options.receipt.skill,
|
|
349
452
|
owner: options.receipt.owner,
|
|
@@ -352,13 +455,18 @@ function buildReceipt(options: StateWriterOptions | undefined): WorkflowStateRec
|
|
|
352
455
|
nowIso: options.receipt.nowIso,
|
|
353
456
|
mutationId: options.receipt.mutationId,
|
|
354
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;
|
|
355
463
|
}
|
|
356
464
|
|
|
357
465
|
async function maybeAudit(mutatedPath: string, options?: StateWriterOptions): Promise<void> {
|
|
358
466
|
if (!options?.audit) return;
|
|
359
467
|
const audit = options.audit;
|
|
360
468
|
const cwd = path.resolve(audit.cwd ?? options.cwd ?? process.cwd());
|
|
361
|
-
await appendAuditEntry(cwd, {
|
|
469
|
+
await appendAuditEntry(cwd, options?.audit?.sessionId ?? "", {
|
|
362
470
|
ts: new Date().toISOString(),
|
|
363
471
|
skill: audit.skill,
|
|
364
472
|
category: audit.category,
|
|
@@ -385,6 +493,118 @@ async function atomicWrite(filePath: string, content: string): Promise<string> {
|
|
|
385
493
|
return filePath;
|
|
386
494
|
}
|
|
387
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 = resolveSkcTarget(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 = resolveSkcTarget(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
|
+
|
|
388
608
|
export async function writeJsonAtomic(
|
|
389
609
|
targetPath: string,
|
|
390
610
|
value: unknown,
|
|
@@ -429,7 +649,7 @@ async function recordInvalidWorkflowTransition(args: {
|
|
|
429
649
|
// internal write skipped a manifest edge.
|
|
430
650
|
const cwd = path.resolve(options?.audit?.cwd ?? options?.cwd ?? process.cwd());
|
|
431
651
|
try {
|
|
432
|
-
await appendAuditEntry(cwd, {
|
|
652
|
+
await appendAuditEntry(cwd, options?.audit?.sessionId ?? "", {
|
|
433
653
|
ts: new Date().toISOString(),
|
|
434
654
|
skill,
|
|
435
655
|
category: "state",
|
|
@@ -736,8 +956,7 @@ export async function removeFileAudited(targetPath: string, options?: StateWrite
|
|
|
736
956
|
}
|
|
737
957
|
|
|
738
958
|
/**
|
|
739
|
-
* Active entry files under `.skc/state/active/<skill>.json`
|
|
740
|
-
* `.skc/state/sessions/<id>/active/<skill>.json` are authoritative. The
|
|
959
|
+
* Active entry files under `.skc/_session-{id}/state/active/<skill>.json` are authoritative. The
|
|
741
960
|
* adjacent `skill-active-state.json` file is only a derived cache rebuilt from
|
|
742
961
|
* those entries, so concurrent snapshot rebuilds can race without losing any
|
|
743
962
|
* writer's per-skill state.
|
|
@@ -750,8 +969,16 @@ export async function writeActiveEntry(
|
|
|
750
969
|
options?: StateWriterOptions,
|
|
751
970
|
): Promise<string> {
|
|
752
971
|
const filePath = activeEntryPath(path.resolve(cwd), sessionScope, skill);
|
|
753
|
-
await
|
|
754
|
-
|
|
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
|
+
);
|
|
755
982
|
return filePath;
|
|
756
983
|
}
|
|
757
984
|
|
|
@@ -762,9 +989,24 @@ export async function removeActiveEntry(
|
|
|
762
989
|
options?: StateWriterOptions,
|
|
763
990
|
): Promise<DeleteResult> {
|
|
764
991
|
const filePath = activeEntryPath(path.resolve(cwd), sessionScope, skill);
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
+
);
|
|
768
1010
|
}
|
|
769
1011
|
|
|
770
1012
|
export async function readActiveEntries(
|
|
@@ -799,8 +1041,14 @@ export async function rebuildActiveSnapshot(
|
|
|
799
1041
|
const resolvedCwd = path.resolve(cwd);
|
|
800
1042
|
const snapshotPath = activeSnapshotPath(resolvedCwd, sessionScope);
|
|
801
1043
|
const entries = await readActiveEntries(resolvedCwd, sessionScope);
|
|
802
|
-
await
|
|
803
|
-
|
|
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
|
+
});
|
|
804
1052
|
return snapshotPath;
|
|
805
1053
|
}
|
|
806
1054
|
|
|
@@ -905,7 +1153,7 @@ export async function hardPrune(
|
|
|
905
1153
|
}
|
|
906
1154
|
if (options?.audit && removed.length > 0) {
|
|
907
1155
|
const audit = options.audit;
|
|
908
|
-
await appendAuditEntry(path.resolve(audit.cwd ?? options.cwd ?? process.cwd()), {
|
|
1156
|
+
await appendAuditEntry(path.resolve(audit.cwd ?? options.cwd ?? process.cwd()), audit.sessionId ?? "", {
|
|
909
1157
|
ts: new Date().toISOString(),
|
|
910
1158
|
skill: audit.skill,
|
|
911
1159
|
category: audit.category,
|
|
@@ -947,26 +1195,41 @@ export async function forceOverwrite(
|
|
|
947
1195
|
);
|
|
948
1196
|
}
|
|
949
1197
|
|
|
950
|
-
export async function appendAuditEntry(
|
|
951
|
-
|
|
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 SKC 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 = resolveSkcTarget(layoutAuditPath(cwd, sessionId), cwd);
|
|
952
1211
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
953
1212
|
await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf-8");
|
|
954
1213
|
return filePath;
|
|
955
1214
|
}
|
|
956
1215
|
|
|
957
|
-
function transactionJournalPath(cwd: string, mutationId: string): string {
|
|
958
|
-
return
|
|
1216
|
+
function transactionJournalPath(cwd: string, sessionId: string, mutationId: string): string {
|
|
1217
|
+
return layoutTransactionJournalPath(path.resolve(cwd), sessionId, mutationId);
|
|
959
1218
|
}
|
|
960
1219
|
|
|
961
1220
|
export async function readWorkflowTransactionJournal(
|
|
962
1221
|
cwd: string,
|
|
1222
|
+
sessionId: string,
|
|
963
1223
|
mutationId: string,
|
|
964
1224
|
): Promise<WorkflowTransactionJournal | undefined> {
|
|
965
|
-
return (await readJsonIfPresent(transactionJournalPath(cwd, mutationId))) as
|
|
1225
|
+
return (await readJsonIfPresent(transactionJournalPath(cwd, sessionId, mutationId))) as
|
|
1226
|
+
| WorkflowTransactionJournal
|
|
1227
|
+
| undefined;
|
|
966
1228
|
}
|
|
967
1229
|
|
|
968
1230
|
export async function beginWorkflowTransactionJournal(input: {
|
|
969
1231
|
cwd: string;
|
|
1232
|
+
sessionId: string;
|
|
970
1233
|
mutationId: string;
|
|
971
1234
|
caller?: CanonicalSkcWorkflowSkill;
|
|
972
1235
|
callee?: CanonicalSkcWorkflowSkill;
|
|
@@ -985,7 +1248,7 @@ export async function beginWorkflowTransactionJournal(input: {
|
|
|
985
1248
|
steps: [],
|
|
986
1249
|
};
|
|
987
1250
|
try {
|
|
988
|
-
return await createJsonNoClobber(transactionJournalPath(input.cwd, input.mutationId), journal, {
|
|
1251
|
+
return await createJsonNoClobber(transactionJournalPath(input.cwd, input.sessionId, input.mutationId), journal, {
|
|
989
1252
|
cwd: input.cwd,
|
|
990
1253
|
});
|
|
991
1254
|
} catch (error) {
|
|
@@ -996,17 +1259,22 @@ export async function beginWorkflowTransactionJournal(input: {
|
|
|
996
1259
|
|
|
997
1260
|
export async function updateWorkflowTransactionJournal(
|
|
998
1261
|
cwd: string,
|
|
1262
|
+
sessionId: string,
|
|
999
1263
|
mutationId: string,
|
|
1000
1264
|
patch: Partial<WorkflowTransactionJournal>,
|
|
1001
1265
|
): Promise<string> {
|
|
1002
|
-
const filePath = transactionJournalPath(cwd, mutationId);
|
|
1266
|
+
const filePath = transactionJournalPath(cwd, sessionId, mutationId);
|
|
1003
1267
|
const current = ((await readJsonIfPresent(filePath)) ?? {}) as WorkflowTransactionJournal;
|
|
1004
1268
|
const next = { ...current, ...patch, updated_at: new Date().toISOString() } as WorkflowTransactionJournal;
|
|
1005
1269
|
await atomicWrite(filePath, jsonText(next));
|
|
1006
1270
|
return filePath;
|
|
1007
1271
|
}
|
|
1008
1272
|
|
|
1009
|
-
export async function completeWorkflowTransactionJournal(
|
|
1010
|
-
|
|
1011
|
-
|
|
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);
|
|
1012
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 { applySkcTmuxProfile, SKC_TMUX_LAUNCHED_ENV } from "./launch-tmux";
|
|
9
|
+
import { modeStatePath, sessionIdFromDirName, sessionReportsDir, teamStateRoot } from "./session-layout";
|
|
10
|
+
import { resolveSkcSessionForWrite, 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}.skc${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.SKC_SESSION_ID?.trim();
|
|
562
|
+
// Session-scoped audit requires a SKC session. When an explicit env-root override
|
|
563
|
+
// (e.g. SKC_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: "skc-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 resolveSkcTeamStateRoot(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
|
|
669
677
|
const explicit = env.SKC_TEAM_STATE_ROOT?.trim();
|
|
670
678
|
if (explicit) return path.resolve(cwd, explicit);
|
|
671
|
-
|
|
679
|
+
const session = resolveSkcSessionForWrite(cwd, { envSessionId: env.SKC_SESSION_ID });
|
|
680
|
+
return teamStateRoot(cwd, session.skcSessionId);
|
|
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 persistSkcTeamModeStateSummary(snapshot: SkcTeamSnapshot, cwd = process.cwd()): Promise<void> {
|
|
1178
1187
|
const active = snapshot.phase !== "complete" && snapshot.phase !== "cancelled";
|
|
1179
1188
|
const updatedAt = now();
|
|
1189
|
+
const sessionId = resolveSkcSessionForWrite(cwd, { envSessionId: process.env.SKC_SESSION_ID }).skcSessionId;
|
|
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 persistSkcTeamModeStateSummary(snapshot: SkcTeamSnapshot,
|
|
|
1195
1206
|
skill: "team",
|
|
1196
1207
|
owner: "skc-runtime",
|
|
1197
1208
|
command: "skc team sync-team-summary",
|
|
1209
|
+
sessionId,
|
|
1198
1210
|
nowIso: updatedAt,
|
|
1199
1211
|
},
|
|
1200
|
-
audit: { category: "state", verb: "sync-team-summary", owner: "skc-runtime", skill: "team" },
|
|
1212
|
+
audit: { category: "state", verb: "sync-team-summary", owner: "skc-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: SkcTeamConfig): string {
|
|
2123
|
-
return path.join(
|
|
2136
|
+
return path.join(
|
|
2137
|
+
sessionReportsDir(
|
|
2138
|
+
config.leader_cwd,
|
|
2139
|
+
resolveSkcSessionForWrite(config.leader_cwd, { envSessionId: process.env.SKC_SESSION_ID }).skcSessionId,
|
|
2140
|
+
),
|
|
2141
|
+
"team-commit-hygiene",
|
|
2142
|
+
`${config.team_name}.ledger.json`,
|
|
2143
|
+
);
|
|
2124
2144
|
}
|
|
2125
2145
|
function integrationNowState(
|
|
2126
2146
|
status: SkcTeamIntegrationStatus,
|
|
@@ -2186,11 +2206,15 @@ export type SkcWorkerCheckpointClassification =
|
|
|
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
|
-
".skc/state/",
|
|
2190
|
-
".skc/logs/",
|
|
2191
|
-
".skc/reports/",
|
|
2192
|
-
".skc/
|
|
2193
|
-
".skc/ultragoal/",
|
|
2209
|
+
".skc/_session-*/state/",
|
|
2210
|
+
".skc/_session-*/logs/",
|
|
2211
|
+
".skc/_session-*/reports/",
|
|
2212
|
+
".skc/_session-*/runtime/",
|
|
2213
|
+
".skc/_session-*/ultragoal/",
|
|
2214
|
+
".skc/_session-*/plans/",
|
|
2215
|
+
".skc/_session-*/specs/",
|
|
2216
|
+
".skc/_session-*/rlm/",
|
|
2217
|
+
".skc/_session-*/audit/",
|
|
2194
2218
|
];
|
|
2195
2219
|
|
|
2196
2220
|
function parsePorcelainStatusFiles(stdout: string): string[] {
|
|
@@ -2211,12 +2235,12 @@ export function classifySkcTeamCheckpointFiles(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 };
|