@cassiomc1/forgeloop 1.0.0 → 1.1.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/.cursor/rules/project-loop.mdc +3 -2
- package/.github/copilot-instructions.md +1 -0
- package/AGENTS.md +2 -1
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +36 -0
- package/ENG/design-code-eng.md +31 -0
- package/ENG/documentation-quality-eng.md +400 -0
- package/EXECUTION_STATE.md +23 -13
- package/GUIDE_ROUTER.md +23 -8
- package/LOOP_ENGINEERING.md +77 -12
- package/PROTOCOL_INTEGRATION.md +9 -6
- package/README.md +82 -39
- package/docs/ARTIFACT_REFERENCE.md +442 -0
- package/docs/CLI_REFERENCE.md +892 -0
- package/docs/CROSS_HARNESS_CONTINUITY.md +198 -0
- package/docs/DOCUMENTATION_GUIDE.md +161 -0
- package/docs/GETTING_STARTED.md +348 -0
- package/docs/RECIPES.md +250 -0
- package/docs/TROUBLESHOOTING.md +345 -0
- package/docs/assets/forgeloop-flow.svg +1 -1
- package/docs/forgeloop-flow.mmd +1 -1
- package/package.json +13 -2
- package/schemas/task-descriptor.schema.json +56 -0
- package/schemas/work-state.schema.json +18 -1
- package/scripts/CI_VALIDATORS.md +7 -0
- package/src/cli.js +280 -388
- package/src/commands/advance.js +5 -2
- package/src/commands/audit.js +11 -1
- package/src/commands/clear-continuity.js +5 -2
- package/src/commands/clear-state.js +5 -2
- package/src/commands/complete.js +9 -1
- package/src/commands/continuity.js +5 -2
- package/src/commands/inspect.js +10 -2
- package/src/commands/next.js +5 -2
- package/src/commands/preflight.js +9 -1
- package/src/commands/prepare-completion.js +5 -2
- package/src/commands/reconcile-continuity.js +5 -2
- package/src/commands/record-check.js +7 -1
- package/src/commands/record-continuity.js +21 -14
- package/src/commands/record-terminal-result.js +7 -1
- package/src/commands/route.js +22 -18
- package/src/commands/run-check.js +52 -44
- package/src/commands/status.js +18 -12
- package/src/commands/task-create.js +94 -0
- package/src/commands/task-list.js +48 -0
- package/src/commands/task-migrate.js +34 -0
- package/src/commands/task-scope.js +75 -0
- package/src/commands/task-show.js +81 -0
- package/src/commands/task-unlock.js +35 -0
- package/src/commands/validate-protocol.js +37 -20
- package/src/commands/validate-state.js +24 -18
- package/src/config/guides.json +42 -0
- package/src/core/activation.js +8 -4
- package/src/core/artifact-registry.js +166 -0
- package/src/core/audit.js +65 -12
- package/src/core/bundles.js +76 -50
- package/src/core/cli-command-definitions.js +611 -0
- package/src/core/cli-metadata.js +23 -0
- package/src/core/completion-artifacts.js +161 -74
- package/src/core/completion.js +134 -76
- package/src/core/continuity.js +20 -13
- package/src/core/contract.js +6 -3
- package/src/core/error-codes.js +197 -0
- package/src/core/events.js +19 -14
- package/src/core/execution.js +38 -6
- package/src/core/gate-artifact.js +12 -9
- package/src/core/gates.js +4 -2
- package/src/core/guide-metadata.js +7 -11
- package/src/core/guide-registry.js +29 -0
- package/src/core/inspect.js +7 -4
- package/src/core/native-adapters.js +6 -0
- package/src/core/phase.js +85 -33
- package/src/core/preflight-consistency.js +24 -14
- package/src/core/preflight-loaders.js +16 -11
- package/src/core/preflight.js +44 -25
- package/src/core/protocol.js +2 -11
- package/src/core/receipt.js +1 -1
- package/src/core/report.js +2 -2
- package/src/core/repository.js +46 -12
- package/src/core/resumability.js +6 -4
- package/src/core/route-artifact.js +9 -5
- package/src/core/router.js +11 -7
- package/src/core/schema-validation.js +1 -0
- package/src/core/task-command.js +41 -0
- package/src/core/task-context.js +126 -0
- package/src/core/task-descriptor.js +81 -0
- package/src/core/task-discovery.js +116 -0
- package/src/core/task-identity.js +76 -0
- package/src/core/task-lock.js +209 -0
- package/src/core/task-migration-validation.js +140 -0
- package/src/core/task-migration.js +361 -0
- package/src/core/task-paths.js +96 -0
- package/src/core/task-scope.js +179 -0
- package/src/core/templates.js +3 -9
- package/src/core/work-state.js +24 -13
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { assertTaskId, taskStorageKey } from "./task-identity.js";
|
|
2
|
+
|
|
3
|
+
export const TASK_STATE_ROOT = ".forgeloop/task-state";
|
|
4
|
+
export const SESSIONS_ROOT = ".forgeloop/sessions";
|
|
5
|
+
|
|
6
|
+
export const TASK_ARTIFACT_FILES = Object.freeze({
|
|
7
|
+
descriptor: "task.json",
|
|
8
|
+
contract: "contract.json",
|
|
9
|
+
route: "routing-result.json",
|
|
10
|
+
preflight: "preflight.json",
|
|
11
|
+
state: "work-state.json",
|
|
12
|
+
continuity: "continuity.json",
|
|
13
|
+
receipt: "execution-receipt.json",
|
|
14
|
+
events: "events.ndjson",
|
|
15
|
+
gates: "gates",
|
|
16
|
+
executions: "executions",
|
|
17
|
+
lock: ".lock",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const PROJECT_ARTIFACT_PATHS = Object.freeze({
|
|
21
|
+
config: ".forgeloop/config.json",
|
|
22
|
+
sources: ".forgeloop/sources.json",
|
|
23
|
+
manifest: ".forgeloop/.manifest.json",
|
|
24
|
+
kit: ".forgeloop/kit",
|
|
25
|
+
gitignore: ".forgeloop/.gitignore",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const LEGACY_TASK_ARTIFACT_PATHS = Object.freeze({
|
|
29
|
+
contract: ".forgeloop/current-contract.json",
|
|
30
|
+
route: ".forgeloop/routing-result.json",
|
|
31
|
+
preflight: ".forgeloop/preflight.json",
|
|
32
|
+
state: ".forgeloop/work-state.json",
|
|
33
|
+
continuity: ".forgeloop/continuity.json",
|
|
34
|
+
receipt: ".forgeloop/execution-receipt.json",
|
|
35
|
+
events: ".forgeloop/events.ndjson",
|
|
36
|
+
gates: ".forgeloop/gates",
|
|
37
|
+
executions: ".forgeloop/executions",
|
|
38
|
+
session: ".forgeloop/session.json",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export function taskDirectory(taskId) {
|
|
42
|
+
return `${TASK_STATE_ROOT}/${taskStorageKey(taskId)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function taskArtifactPath(taskId, key) {
|
|
46
|
+
const relative = TASK_ARTIFACT_FILES[key];
|
|
47
|
+
if (!relative) {
|
|
48
|
+
throw new Error(`Unknown task artifact: ${key}`);
|
|
49
|
+
}
|
|
50
|
+
return `${taskDirectory(taskId)}/${relative}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function taskGatePath(taskId, gateId) {
|
|
54
|
+
if (typeof gateId !== "string" || !gateId || gateId.includes("/") || gateId.includes("\\") || gateId.includes("..")) {
|
|
55
|
+
throw new Error(`Invalid gate ID: ${gateId}`);
|
|
56
|
+
}
|
|
57
|
+
const cleanId = gateId.endsWith(".json") ? gateId : `${gateId}.json`;
|
|
58
|
+
return `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.gates}/${cleanId}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function taskExecutionPath(taskId, executionId) {
|
|
62
|
+
if (typeof executionId !== "string" || !/^exec-[A-Za-z0-9_-]+$/.test(executionId)) {
|
|
63
|
+
throw new Error(`Invalid execution ID: ${executionId}`);
|
|
64
|
+
}
|
|
65
|
+
return `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.executions}/${executionId}.json`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function taskLockPath(taskId) {
|
|
69
|
+
return taskArtifactPath(taskId, "lock");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function sessionArtifactPath(sessionId) {
|
|
73
|
+
if (typeof sessionId !== "string" || !sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
74
|
+
throw new Error(`Invalid session ID: ${sessionId}`);
|
|
75
|
+
}
|
|
76
|
+
const cleanId = sessionId.endsWith(".json") ? sessionId : `${sessionId}.json`;
|
|
77
|
+
return `${SESSIONS_ROOT}/${cleanId}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildTaskArtifactPaths(taskId) {
|
|
81
|
+
assertTaskId(taskId);
|
|
82
|
+
const dir = taskDirectory(taskId);
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
descriptor: `${dir}/${TASK_ARTIFACT_FILES.descriptor}`,
|
|
85
|
+
contract: `${dir}/${TASK_ARTIFACT_FILES.contract}`,
|
|
86
|
+
route: `${dir}/${TASK_ARTIFACT_FILES.route}`,
|
|
87
|
+
preflight: `${dir}/${TASK_ARTIFACT_FILES.preflight}`,
|
|
88
|
+
state: `${dir}/${TASK_ARTIFACT_FILES.state}`,
|
|
89
|
+
continuity: `${dir}/${TASK_ARTIFACT_FILES.continuity}`,
|
|
90
|
+
receipt: `${dir}/${TASK_ARTIFACT_FILES.receipt}`,
|
|
91
|
+
events: `${dir}/${TASK_ARTIFACT_FILES.events}`,
|
|
92
|
+
gates: `${dir}/${TASK_ARTIFACT_FILES.gates}`,
|
|
93
|
+
executions: `${dir}/${TASK_ARTIFACT_FILES.executions}`,
|
|
94
|
+
lock: `${dir}/${TASK_ARTIFACT_FILES.lock}`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
E_TASK_CHANGE_OUTSIDE_SCOPE,
|
|
4
|
+
E_TASK_DESCRIPTOR_INVALID,
|
|
5
|
+
E_TASK_SCOPE_CONFLICT,
|
|
6
|
+
E_TASK_SCOPE_DIRTY,
|
|
7
|
+
E_TASK_SCOPE_FROZEN,
|
|
8
|
+
} from "./error-codes.js";
|
|
9
|
+
import { currentChangedPaths } from "./repository.js";
|
|
10
|
+
|
|
11
|
+
const FROZEN_PHASES = new Set([
|
|
12
|
+
"EXECUTING",
|
|
13
|
+
"VERIFYING",
|
|
14
|
+
"DIAGNOSING",
|
|
15
|
+
"CORRECTING",
|
|
16
|
+
"REVIEWING",
|
|
17
|
+
"COMPLETE",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export function normalizeWriteClaim(claim) {
|
|
21
|
+
if (typeof claim !== "string" || claim.trim() === "") {
|
|
22
|
+
const error = new Error("Write claim must be a non-empty string");
|
|
23
|
+
error.code = E_TASK_DESCRIPTOR_INVALID;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const portable = claim.trim().replaceAll("\\", "/");
|
|
28
|
+
if (portable.startsWith("/") || /^[A-Za-z]:\//.test(portable)) {
|
|
29
|
+
const error = new Error(`Write claim must remain relative: ${claim}`);
|
|
30
|
+
error.code = E_TASK_DESCRIPTOR_INVALID;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const normalized = path.posix.normalize(portable);
|
|
35
|
+
if (normalized === ".." || normalized.startsWith("../")) {
|
|
36
|
+
const error = new Error(`Write claim escapes project directory: ${claim}`);
|
|
37
|
+
error.code = E_TASK_DESCRIPTOR_INVALID;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (normalized === "." || normalized === "./") {
|
|
42
|
+
return ".";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return normalized.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeWriteClaims(claims) {
|
|
49
|
+
if (claims === undefined || claims === null) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
if (!Array.isArray(claims)) {
|
|
53
|
+
const error = new Error("writeClaims must be an array of string prefixes");
|
|
54
|
+
error.code = E_TASK_DESCRIPTOR_INVALID;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const normalized = claims.map((c) => normalizeWriteClaim(c));
|
|
59
|
+
const unique = [...new Set(normalized)];
|
|
60
|
+
return unique.sort((a, b) => a.localeCompare(b));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function claimsOverlap(claimA, claimB) {
|
|
64
|
+
const normA = normalizeWriteClaim(claimA).toLowerCase();
|
|
65
|
+
const normB = normalizeWriteClaim(claimB).toLowerCase();
|
|
66
|
+
|
|
67
|
+
if (normA === "." || normB === ".") {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (normA === normB) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (normA.startsWith(`${normB}/`)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (normB.startsWith(`${normA}/`)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function checkScopeConflicts(newClaims, existingTasks = [], currentTaskId = null) {
|
|
87
|
+
const normalizedNew = normalizeWriteClaims(newClaims);
|
|
88
|
+
if (normalizedNew.length === 0) return [];
|
|
89
|
+
|
|
90
|
+
const conflicts = [];
|
|
91
|
+
for (const task of existingTasks) {
|
|
92
|
+
if (task.taskId === currentTaskId) continue;
|
|
93
|
+
// Only non-COMPLETE tasks hold active write claims
|
|
94
|
+
if (task.phase === "COMPLETE") continue;
|
|
95
|
+
|
|
96
|
+
const taskClaims = normalizeWriteClaims(task.writeClaims ?? task.descriptor?.writeClaims ?? []);
|
|
97
|
+
for (const newClaim of normalizedNew) {
|
|
98
|
+
for (const existingClaim of taskClaims) {
|
|
99
|
+
if (claimsOverlap(newClaim, existingClaim)) {
|
|
100
|
+
conflicts.push({
|
|
101
|
+
taskId: task.taskId,
|
|
102
|
+
phase: task.phase,
|
|
103
|
+
conflictingClaim: existingClaim,
|
|
104
|
+
requestedClaim: newClaim,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return conflicts;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function assertNoScopeConflicts(newClaims, existingTasks = [], currentTaskId = null) {
|
|
115
|
+
const conflicts = checkScopeConflicts(newClaims, existingTasks, currentTaskId);
|
|
116
|
+
if (conflicts.length > 0) {
|
|
117
|
+
const first = conflicts[0];
|
|
118
|
+
const error = new Error(
|
|
119
|
+
`Task write claim "${first.requestedClaim}" conflicts with active task "${first.taskId}" (claim "${first.conflictingClaim}", phase: ${first.phase ?? "PLANNED"})`,
|
|
120
|
+
);
|
|
121
|
+
error.code = E_TASK_SCOPE_CONFLICT;
|
|
122
|
+
error.conflicts = conflicts;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function assertScopeClean(target, claims) {
|
|
128
|
+
const normalized = normalizeWriteClaims(claims);
|
|
129
|
+
if (normalized.length === 0) return;
|
|
130
|
+
|
|
131
|
+
const changed = await currentChangedPaths(target, { paths: normalized });
|
|
132
|
+
if (changed !== null && changed.length > 0) {
|
|
133
|
+
const error = new Error(
|
|
134
|
+
`Claimed scope contains pre-existing uncommitted changes: ${changed.slice(0, 5).join(", ")}${changed.length > 5 ? "..." : ""}`,
|
|
135
|
+
);
|
|
136
|
+
error.code = E_TASK_SCOPE_DIRTY;
|
|
137
|
+
error.changedPaths = changed;
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function assertScopeNotFrozen(phase) {
|
|
143
|
+
if (phase && FROZEN_PHASES.has(phase)) {
|
|
144
|
+
const error = new Error(
|
|
145
|
+
`Task scope cannot be modified once execution lifecycle has started (current phase: ${phase})`,
|
|
146
|
+
);
|
|
147
|
+
error.code = E_TASK_SCOPE_FROZEN;
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function assertClaimsCoverChangedPaths(claims, changedPaths) {
|
|
153
|
+
const normalizedClaims = normalizeWriteClaims(claims);
|
|
154
|
+
if (normalizedClaims.length === 0) return;
|
|
155
|
+
|
|
156
|
+
// Root claim covers everything
|
|
157
|
+
if (normalizedClaims.includes(".")) return;
|
|
158
|
+
|
|
159
|
+
const outOfScope = [];
|
|
160
|
+
for (const changedPath of changedPaths ?? []) {
|
|
161
|
+
const normPath = changedPath.replaceAll("\\", "/").replace(/^\.\//, "").toLowerCase();
|
|
162
|
+
const covered = normalizedClaims.some((claim) => {
|
|
163
|
+
const normClaim = claim.toLowerCase();
|
|
164
|
+
return normPath === normClaim || normPath.startsWith(`${normClaim}/`);
|
|
165
|
+
});
|
|
166
|
+
if (!covered) {
|
|
167
|
+
outOfScope.push(changedPath);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (outOfScope.length > 0) {
|
|
172
|
+
const error = new Error(
|
|
173
|
+
`Observed repository changes exceed task write claims: ${outOfScope.slice(0, 5).join(", ")}${outOfScope.length > 5 ? "..." : ""}`,
|
|
174
|
+
);
|
|
175
|
+
error.code = E_TASK_CHANGE_OUTSIDE_SCOPE;
|
|
176
|
+
error.outOfScopePaths = outOfScope;
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
package/src/core/templates.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
targetPathForSource,
|
|
9
9
|
} from "./target-layout.js";
|
|
10
10
|
import { nativeShim } from "./native-adapters.js";
|
|
11
|
+
import { GUIDE_TEMPLATE_PATHS } from "./guide-registry.js";
|
|
11
12
|
|
|
12
13
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
13
14
|
|
|
@@ -37,15 +38,7 @@ export const TEMPLATE_PATHS = [
|
|
|
37
38
|
"THIRD_PARTY_NOTICES.md",
|
|
38
39
|
"LICENSE",
|
|
39
40
|
"LICENSE-DOCS.md",
|
|
40
|
-
|
|
41
|
-
"ENG/clean-code-eng.md",
|
|
42
|
-
"ENG/design-code-eng.md",
|
|
43
|
-
"ENG/games-code-design-web-eng.md",
|
|
44
|
-
"ENG/perf-code-eng.md",
|
|
45
|
-
"ENG/premium-sites-studio-eng.md",
|
|
46
|
-
"ENG/taste-frontend-eng.md",
|
|
47
|
-
"ENG/sec-code-eng.md",
|
|
48
|
-
"ENG/test-code-eng.md",
|
|
41
|
+
...GUIDE_TEMPLATE_PATHS,
|
|
49
42
|
"schemas/routing-input.schema.json",
|
|
50
43
|
"schemas/routing-result.schema.json",
|
|
51
44
|
"schemas/work-state.schema.json",
|
|
@@ -67,6 +60,7 @@ export const TEMPLATE_PATHS = [
|
|
|
67
60
|
"schemas/policy.schema.json",
|
|
68
61
|
"schemas/task-bundle.schema.json",
|
|
69
62
|
"schemas/authority.schema.json",
|
|
63
|
+
"schemas/task-descriptor.schema.json",
|
|
70
64
|
];
|
|
71
65
|
|
|
72
66
|
export function getPackageRoot() {
|
package/src/core/work-state.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createEvidence } from "./evidence.js";
|
|
|
18
18
|
import { assertJsonBytes } from "./json-safety.js";
|
|
19
19
|
import { assertCoverageList } from "./coverage.js";
|
|
20
20
|
import { canonicalFingerprint } from "./artifacts.js";
|
|
21
|
+
import { taskArtifactPath } from "./task-paths.js";
|
|
21
22
|
|
|
22
23
|
export const WORK_STATE_PATH = ".forgeloop/work-state.json";
|
|
23
24
|
|
|
@@ -229,17 +230,22 @@ async function validateStoredState(state, packageRoot = getPackageRoot()) {
|
|
|
229
230
|
return assertWorkStateSemantics(state);
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
export async function readWorkState(target,
|
|
233
|
-
|
|
234
|
-
const
|
|
233
|
+
export async function readWorkState(target, options = {}) {
|
|
234
|
+
const packageRoot = typeof options === "string" ? options : (options?.packageRoot ?? getPackageRoot());
|
|
235
|
+
const relPath = typeof options === "object" && options !== null
|
|
236
|
+
? (options.statePath ?? options.relativePath ?? (options.taskId ? taskArtifactPath(options.taskId, "state") : (options.taskContext ? options.taskContext.paths.state : WORK_STATE_PATH)))
|
|
237
|
+
: WORK_STATE_PATH;
|
|
238
|
+
|
|
239
|
+
await assertSafePath(target, relPath);
|
|
240
|
+
const statePath = ensureWithin(target, relPath);
|
|
235
241
|
if (!(await fileExists(statePath))) return null;
|
|
236
242
|
let state;
|
|
237
243
|
try {
|
|
238
244
|
const bytes = await readBytes(statePath);
|
|
239
|
-
assertJsonBytes(bytes,
|
|
245
|
+
assertJsonBytes(bytes, relPath);
|
|
240
246
|
state = JSON.parse(bytes.toString("utf8"));
|
|
241
247
|
} catch (error) {
|
|
242
|
-
throw new WorkStateError(`Unable to parse ${
|
|
248
|
+
throw new WorkStateError(`Unable to parse ${relPath}: ${error.message}`);
|
|
243
249
|
}
|
|
244
250
|
try {
|
|
245
251
|
return await validateStoredState(state, packageRoot);
|
|
@@ -263,20 +269,25 @@ export async function readContractFingerprint(target, contractFile) {
|
|
|
263
269
|
return { path: contractFile, fingerprint: contractFingerprint(contract) };
|
|
264
270
|
}
|
|
265
271
|
|
|
266
|
-
export async function writeWorkState(target, state,
|
|
272
|
+
export async function writeWorkState(target, state, options = {}) {
|
|
273
|
+
const packageRoot = options?.packageRoot ?? getPackageRoot();
|
|
274
|
+
const dryRun = options?.dryRun ?? false;
|
|
275
|
+
const relPath = options?.statePath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "state") : WORK_STATE_PATH);
|
|
276
|
+
|
|
267
277
|
await validateStoredState(state, packageRoot);
|
|
268
|
-
await assertSafePath(target,
|
|
269
|
-
const statePath = ensureWithin(target,
|
|
278
|
+
await assertSafePath(target, relPath);
|
|
279
|
+
const statePath = ensureWithin(target, relPath);
|
|
270
280
|
await writeFileAtomic(statePath, `${JSON.stringify(state, null, 2)}\n`, { dryRun });
|
|
271
281
|
return state;
|
|
272
282
|
}
|
|
273
283
|
|
|
274
|
-
export async function clearWorkState(target) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
export async function clearWorkState(target, options = {}) {
|
|
285
|
+
const relPath = options?.statePath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "state") : WORK_STATE_PATH);
|
|
286
|
+
await assertSafePath(target, relPath);
|
|
287
|
+
const statePath = ensureWithin(target, relPath);
|
|
288
|
+
if (!(await fileExists(statePath))) return { removed: false, path: relPath };
|
|
278
289
|
await unlink(statePath);
|
|
279
|
-
return { removed: true, path:
|
|
290
|
+
return { removed: true, path: relPath };
|
|
280
291
|
}
|
|
281
292
|
|
|
282
293
|
export async function readRequiredArtifactFingerprints(target, artifacts) {
|