@playcraft/cli 0.1.57-beta → 0.1.59-beta
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/dist/commands/workspace.js +43 -0
- package/dist/utils/agent-generation-provenance.js +1 -1
- package/dist/workspace/messages.js +10 -0
- package/dist/workspace-runtime/batch/deliver.js +140 -80
- package/dist/workspace-runtime/batch/freeze.js +18 -6
- package/dist/workspace-runtime/batch/generate.js +68 -64
- package/dist/workspace-runtime/batch/policy.js +71 -22
- package/dist/workspace-runtime/batch/prepare.js +108 -93
- package/dist/workspace-runtime/batch/records.js +4 -0
- package/dist/workspace-runtime/batch/routes.js +10 -2
- package/dist/workspace-runtime/batch/source-candidate.js +2 -0
- package/dist/workspace-runtime/batch/source-capture.js +1 -0
- package/dist/workspace-runtime/batch/source-failure.js +2 -0
- package/dist/workspace-runtime/batch/source-publish.js +3 -0
- package/dist/workspace-runtime/batch/writer-proof.js +37 -0
- package/dist/workspace-runtime/codex/app-server.js +278 -20
- package/dist/workspace-runtime/codex/config-toml.js +4 -0
- package/dist/workspace-runtime/codex/delivery-tools.js +1 -1
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +17 -8
- package/dist/workspace-runtime/codex/loopback.js +2 -0
- package/dist/workspace-runtime/codex/model-broker.js +7 -4
- package/dist/workspace-runtime/persistence/journal.js +34 -0
- package/dist/workspace-runtime/persistence/sqlite-worker.js +68 -6
- package/dist/workspace-runtime/previews/output.js +2 -0
- package/dist/workspace-runtime/previews/publish.js +20 -5
- package/dist/workspace-runtime/previews/runtime-preview.js +4 -0
- package/dist/workspace-runtime/processes/managed-writes.js +47 -5
- package/dist/workspace-runtime/serve.js +13 -3
- package/dist/workspace-runtime/server/accept-operation.js +22 -2
- package/dist/workspace-runtime/server/directory-access.js +414 -0
- package/dist/workspace-runtime/server/dispatch.js +74 -29
- package/dist/workspace-runtime/server/endpoints.js +1 -0
- package/dist/workspace-runtime/server/http.js +80 -10
- package/dist/workspace-runtime/server/operation-record.js +2 -0
- package/dist/workspace-runtime/server/terminal-host.js +124 -8
- package/dist/workspace-runtime/server/workspace-storage.js +193 -0
- package/dist/workspace-runtime/server/workspace-transfer.js +3 -1
- package/dist/workspace-runtime/workspaces/append-only-log.js +3 -0
- package/dist/workspace-runtime/workspaces/artifact-events.js +5 -2
- package/dist/workspace-runtime/workspaces/artifact-files.js +14 -8
- package/dist/workspace-runtime/workspaces/artifact-receipts.js +7 -25
- package/dist/workspace-runtime/workspaces/artifacts.js +4 -1
- package/dist/workspace-runtime/workspaces/asset-library-import.js +9 -4
- package/dist/workspace-runtime/workspaces/asset-publication-access.js +7 -9
- package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +3 -0
- package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +17 -2
- package/dist/workspace-runtime/workspaces/attachments.js +9 -0
- package/dist/workspace-runtime/workspaces/batch-commit.js +5 -3
- package/dist/workspace-runtime/workspaces/binary-write.js +12 -7
- package/dist/workspace-runtime/workspaces/cli-context.js +2 -0
- package/dist/workspace-runtime/workspaces/cli-credentials.js +1 -0
- package/dist/workspace-runtime/workspaces/delivery-build.js +30 -11
- package/dist/workspace-runtime/workspaces/delivery-store.js +2 -0
- package/dist/workspace-runtime/workspaces/dependencies.js +90 -70
- package/dist/workspace-runtime/workspaces/developer-command.js +12 -1
- package/dist/workspace-runtime/workspaces/directory-access.js +13 -18
- package/dist/workspace-runtime/workspaces/directory-attachments.js +8 -0
- package/dist/workspace-runtime/workspaces/directory-create.js +18 -5
- package/dist/workspace-runtime/workspaces/directory-maintenance.js +46 -35
- package/dist/workspace-runtime/workspaces/directory-mutations.js +8 -3
- package/dist/workspace-runtime/workspaces/files.js +4 -0
- package/dist/workspace-runtime/workspaces/git-credential-broker.js +3 -0
- package/dist/workspace-runtime/workspaces/git-direct-store.js +2 -0
- package/dist/workspace-runtime/workspaces/json5-edit.js +36 -0
- package/dist/workspace-runtime/workspaces/library-attachments.js +4 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +2 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-store.js +2 -0
- package/dist/workspace-runtime/workspaces/parameter-agent.js +40 -11
- package/dist/workspace-runtime/workspaces/parameter-assets.js +1 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill-transaction.js +141 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +329 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization.js +63 -0
- package/dist/workspace-runtime/workspaces/parameter-production.js +124 -0
- package/dist/workspace-runtime/workspaces/parameters.js +95 -3
- package/dist/workspace-runtime/workspaces/platform-export.js +5 -0
- package/dist/workspace-runtime/workspaces/prepare.js +4 -0
- package/dist/workspace-runtime/workspaces/registry.js +55 -3
- package/dist/workspace-runtime/workspaces/remix-references.js +5 -0
- package/dist/workspace-runtime/workspaces/shared-write.js +187 -0
- package/dist/workspace-runtime/workspaces/skills.js +14 -15
- package/dist/workspace-runtime/workspaces/storage-snapshot.js +235 -0
- package/dist/workspace-runtime/workspaces/transfer.js +4 -1
- package/dist/workspace-runtime/workspaces/workspace-shares.js +235 -0
- package/dist/workspace-runtime/workspaces/worktree-attachments.js +2 -0
- package/dist/workspace-runtime/workspaces/worktree-checkout.js +11 -6
- package/package.json +5 -4
- package/project-template/.claude/skills/playcraft-runtime-parameters/SKILL.md +32 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/references/platform-protocol.md +21 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/references/preview-integration.md +57 -0
|
@@ -33,6 +33,42 @@ export function editJson5Paths(source, changes) {
|
|
|
33
33
|
JSON5.parse(next);
|
|
34
34
|
return next;
|
|
35
35
|
}
|
|
36
|
+
/** Replace complete language records while preserving every unrelated authored member. */
|
|
37
|
+
export function editJson5StringRecords(source, changes) {
|
|
38
|
+
const parsed = JSON5.parse(source);
|
|
39
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
40
|
+
throw new ParameterEditError('parameter_document_invalid');
|
|
41
|
+
if (new Set(changes.map(change => change.key)).size !== changes.length)
|
|
42
|
+
throw new ParameterEditError('parameter_changes_overlap');
|
|
43
|
+
let next = source;
|
|
44
|
+
for (const change of changes) {
|
|
45
|
+
if (!change.key || ['__proto__', 'prototype', 'constructor'].includes(change.key))
|
|
46
|
+
throw new ParameterEditError('parameter_value_invalid');
|
|
47
|
+
const object = readJson5Object(next, skipTrivia(next, 0, next.length));
|
|
48
|
+
const memberIndex = object.members.findIndex(member => member.name === change.key);
|
|
49
|
+
const member = object.members[memberIndex];
|
|
50
|
+
if ('op' in change) {
|
|
51
|
+
if (!member)
|
|
52
|
+
continue;
|
|
53
|
+
const comma = member.comma ?? object.members[memberIndex - 1]?.comma;
|
|
54
|
+
const spans = [{ start: member.keyStart, end: member.value.end }, ...(comma === undefined ? [] : [{ start: comma, end: comma + 1 }])].sort((left, right) => right.start - left.start);
|
|
55
|
+
for (const span of spans)
|
|
56
|
+
next = next.slice(0, span.start) + next.slice(span.end);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
if (!change.value || typeof change.value !== 'object' || Array.isArray(change.value) || Object.values(change.value).some(value => typeof value !== 'string'))
|
|
60
|
+
throw new ParameterEditError('parameter_value_invalid');
|
|
61
|
+
if (!member)
|
|
62
|
+
next = editJson5Paths(next, [{ fieldPath: [change.key], value: '' }]);
|
|
63
|
+
const span = findJson5ValueSpan(next, [change.key]);
|
|
64
|
+
if (!span)
|
|
65
|
+
throw new ParameterEditError('parameter_field_missing');
|
|
66
|
+
next = next.slice(0, span.start) + JSON.stringify(change.value) + next.slice(span.end);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
JSON5.parse(next);
|
|
70
|
+
return next;
|
|
71
|
+
}
|
|
36
72
|
function editJson5Object(source, braceIndex, change, depth) {
|
|
37
73
|
const object = readJson5Object(source, braceIndex);
|
|
38
74
|
const memberIndex = object.members.findIndex((member) => member.name === change.fieldPath[depth]);
|
|
@@ -11,6 +11,7 @@ async function safeDirectory(root, path, create = false) {
|
|
|
11
11
|
let directory = root;
|
|
12
12
|
for (const part of path.split('/').filter(Boolean)) {
|
|
13
13
|
directory = join(directory, part);
|
|
14
|
+
// shared
|
|
14
15
|
if (create)
|
|
15
16
|
await mkdir(directory, { mode: 0o755 }).catch((error) => {
|
|
16
17
|
if (error.code !== 'EEXIST')
|
|
@@ -173,12 +174,14 @@ async function importAttachment(root, body) {
|
|
|
173
174
|
if (!record) {
|
|
174
175
|
const temporary = join(uploads, `.import-${randomUUID()}`);
|
|
175
176
|
const target = join(uploads, id);
|
|
177
|
+
// shared
|
|
176
178
|
await mkdir(temporary, { mode: 0o755 });
|
|
177
179
|
try {
|
|
178
180
|
for (const file of files) {
|
|
179
181
|
const parent = dirname(file.path);
|
|
180
182
|
if (parent !== '.')
|
|
181
183
|
await safeDirectory(temporary, parent, true);
|
|
184
|
+
// shared
|
|
182
185
|
await writeFile(join(temporary, file.path), file.bytes, { flag: 'wx', mode: 0o644 });
|
|
183
186
|
}
|
|
184
187
|
const prefix = `${WORKSPACE_USER_UPLOADS_DIRECTORY}/${id}`;
|
|
@@ -236,6 +239,7 @@ async function importAttachment(root, body) {
|
|
|
236
239
|
record.clientRequestIds = [...new Set([...record.clientRequestIds, clientRequestId])];
|
|
237
240
|
record.unbound ||= !sessionId;
|
|
238
241
|
const tempMetadata = join(metadata, `.import-${randomUUID()}.json`);
|
|
242
|
+
// shared
|
|
239
243
|
await writeFile(tempMetadata, JSON.stringify(record), { flag: 'wx', mode: 0o644 });
|
|
240
244
|
await rename(tempMetadata, join(metadata, `${id}.json`));
|
|
241
245
|
await chmod(join(uploads, id), 0o755);
|
|
@@ -38,3 +38,5 @@ Report JSON (JavaScript Object Notation, JavaScript 对象表示法) contract (n
|
|
|
38
38
|
- No passed/ready/live/checks fields are accepted in the report. All implementation and validation statements remain claims until platform verification.
|
|
39
39
|
- If blocked or partial, preserve reviewable changes and list concrete missing work. Do not overwrite later user edits or claim multi-file changes were atomic.
|
|
40
40
|
`;
|
|
41
|
+
/** Initialization strategy comes from the locked skill; report validation stays platform-owned. */
|
|
42
|
+
export const PARAMETER_AGENT_REPORT_PROTOCOL = PARAMETER_AGENT_PROTOCOL.slice(PARAMETER_AGENT_PROTOCOL.indexOf('Report JSON'));
|
|
@@ -17,6 +17,7 @@ async function taskDirectory(state, workspaceId, directoryName, root) {
|
|
|
17
17
|
}
|
|
18
18
|
const scope = [state.config.environment, state.config.userId, state.config.runtimeId, workspaceId, directoryName];
|
|
19
19
|
const directory = join(parent, parameterDigest(JSON.stringify(scope)));
|
|
20
|
+
// private
|
|
20
21
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
21
22
|
if ((await lstat(parent)).isSymbolicLink() || (await lstat(directory)).isSymbolicLink())
|
|
22
23
|
throw new ParameterAgentError('parameter_agent_state_invalid');
|
|
@@ -47,6 +48,7 @@ export async function writePrivateParameterTask(state, task, root) {
|
|
|
47
48
|
const directory = await taskDirectory(state, task.record.workspaceId, task.record.directoryName, root);
|
|
48
49
|
const record = parameterAgentTaskRecordSchema.parse(task.record);
|
|
49
50
|
const temporary = join(directory, `.${record.taskId}.${randomUUID()}.tmp`);
|
|
51
|
+
// private
|
|
50
52
|
await writeFile(temporary, JSON.stringify({ ...task, record }), { mode: 0o600 });
|
|
51
53
|
await rename(temporary, join(directory, `${record.taskId}.json`));
|
|
52
54
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { parameterAgentReportSchema, parameterAgentTaskRequestSchema, parameterAgentVerifyPayloadSchema,
|
|
2
|
+
import { parameterAgentReportSchema, parameterAgentTaskRequestSchema, parameterAgentVerifyPayloadSchema, parameterI18nCatalog, stableStringify, validateParameterChange, validateParameterI18nChange, } from '@playcraft/common/cloud-workspace/runtime';
|
|
3
3
|
import { readWorkspaceParameters } from './parameters.js';
|
|
4
4
|
import { revisionScope } from './registry.js';
|
|
5
5
|
import { runDeveloperCommand } from './developer-command.js';
|
|
6
|
-
import { PARAMETER_AGENT_PROTOCOL } from './parameter-agent-protocol.js';
|
|
6
|
+
import { PARAMETER_AGENT_PROTOCOL, PARAMETER_AGENT_REPORT_PROTOCOL } from './parameter-agent-protocol.js';
|
|
7
|
+
import { inspectParameterInitialization } from './parameter-initialization.js';
|
|
8
|
+
import { inspectParameterProductionCheck, prepareParameterProductionCheck } from './parameter-production.js';
|
|
7
9
|
import { ParameterAgentError, parameterDigest, parameterInputSnapshot, readParameterAgentFile } from './parameter-agent-inputs.js';
|
|
8
10
|
import { listPrivateParameterTaskIds, parameterTaskWorkspace, readPrivateParameterTask, writePrivateParameterTask } from './parameter-agent-store.js';
|
|
9
11
|
export { ParameterAgentError } from './parameter-agent-inputs.js';
|
|
@@ -58,14 +60,19 @@ async function inspectReport(root, task, snapshot) {
|
|
|
58
60
|
}
|
|
59
61
|
return report;
|
|
60
62
|
}
|
|
61
|
-
export async function prepareParameterAgentTask(state, operation, value, root) {
|
|
63
|
+
export async function prepareParameterAgentTask(state, operation, value, root, options = {}) {
|
|
62
64
|
const request = parameterAgentTaskRequestSchema.parse(value);
|
|
63
65
|
if (operation.kind !== 'session.turn' || operation.clientRequestId !== request.taskId)
|
|
64
66
|
throw new ParameterAgentError('parameter_agent_task_identity_invalid');
|
|
65
67
|
const owner = scope(state, operation);
|
|
68
|
+
const skill = request.action === 'initialize' ? options.initializationSkill : undefined;
|
|
69
|
+
if (request.action === 'initialize' && (!skill || !skill.entryKey || !/^[a-f0-9]{64}$/.test(skill.lockHash) || !skill.path))
|
|
70
|
+
throw new ParameterAgentError('parameter_initialization_skill_unavailable');
|
|
66
71
|
const existing = await readPrivateParameterTask(state, owner.workspaceId, owner.directoryName, request.taskId, root);
|
|
67
72
|
if (existing && (existing.record.sessionId !== owner.sessionId || !current(state, existing) || stableStringify(existing.request) !== stableStringify(request)))
|
|
68
73
|
throw new ParameterAgentError('parameter_agent_task_conflict');
|
|
74
|
+
if (existing && skill && (existing.record.initializationSkill?.entryKey !== skill.entryKey || existing.record.initializationSkill.lockHash !== skill.lockHash))
|
|
75
|
+
throw new ParameterAgentError('parameter_initialization_skill_changed');
|
|
69
76
|
const document = await readWorkspaceParameters(root, state.journal.fileRevisions(revisionScope(state, operation.workspaceId, owner.directoryName)));
|
|
70
77
|
if (request.sourceFingerprint && request.sourceFingerprint !== document.sourceFingerprint)
|
|
71
78
|
throw new ParameterAgentError('parameter_source_conflict');
|
|
@@ -84,7 +91,7 @@ export async function prepareParameterAgentTask(state, operation, value, root) {
|
|
|
84
91
|
const timestamp = now();
|
|
85
92
|
const task = existing ?? {
|
|
86
93
|
request, initialDigest: initial.digest, instanceToken: state.instanceToken,
|
|
87
|
-
record: { version: 1, taskId: request.taskId, action: request.action, ...(request.goal ? { goal: request.goal } : {}), ...owner, generation: state.config.generation, runtimeEpoch: state.runtimeEpoch, status: 'running', reportPath: reportPath(request.taskId), checks: [], createdAt: timestamp, updatedAt: timestamp },
|
|
94
|
+
record: { version: 1, taskId: request.taskId, action: request.action, ...(request.goal ? { goal: request.goal } : {}), ...(skill ? { initializationSkill: { entryKey: skill.entryKey, lockHash: skill.lockHash } } : {}), ...owner, generation: state.config.generation, runtimeEpoch: state.runtimeEpoch, status: 'running', reportPath: reportPath(request.taskId), checks: [], createdAt: timestamp, updatedAt: timestamp },
|
|
88
95
|
};
|
|
89
96
|
if (!existing) {
|
|
90
97
|
if ((await listPrivateParameterTaskIds(state, owner.workspaceId, owner.directoryName, root)).length >= 256)
|
|
@@ -92,9 +99,12 @@ export async function prepareParameterAgentTask(state, operation, value, root) {
|
|
|
92
99
|
await writePrivateParameterTask(state, task, root);
|
|
93
100
|
}
|
|
94
101
|
const instructions = [
|
|
95
|
-
|
|
102
|
+
...(skill ? [
|
|
103
|
+
`This initialization task explicitly selects the enabled, locked parameter skill ${JSON.stringify({ entryKey: skill.entryKey, lockHash: skill.lockHash, path: skill.path })}. Read that frozen SKILL.md and its referenced files before applying its integration workflow. Preserve the current execution permissions.`,
|
|
104
|
+
PARAMETER_AGENT_REPORT_PROTOCOL,
|
|
105
|
+
] : [PARAMETER_AGENT_PROTOCOL]),
|
|
96
106
|
`Platform task identity: ${JSON.stringify({ version: 1, taskId: request.taskId, action: request.action, reportPath: task.record.reportPath })}. Write only this exact report identity.`,
|
|
97
|
-
request.action === 'discover' ? 'Discovery is read-only for business source/schema/data/manifest. Write only the requested development report. Discover actual useful candidates and consumer evidence; do not implement them yet.' : 'Complete the requested integration using existing engineering tools: reuse an existing equivalent field or implement declaration, valid defaults, sparse data semantics, labels, real production consumers and meaningful parameter tests. Leave precise blocked work if incomplete.',
|
|
107
|
+
request.action === 'initialize' ? 'Apply the selected initialization skill to this current game and write its exact admission report. The platform will verify files and checks, then prepare the first preview separately. Do not self-declare a connected or live state.' : request.action === 'discover' ? 'Discovery is read-only for business source/schema/data/manifest. Write only the requested development report. Discover actual useful candidates and consumer evidence; do not implement them yet.' : 'Complete the requested integration using existing engineering tools: reuse an existing equivalent field or implement declaration, valid defaults, sparse data semantics, labels, real production consumers and meaningful parameter tests. Leave precise blocked work if incomplete.',
|
|
98
108
|
`User goal (task data): ${JSON.stringify(request.goal ?? '')}`,
|
|
99
109
|
...(selected ? [`Selected current candidate (reference data): ${JSON.stringify(selected)}`] : []),
|
|
100
110
|
'After finishing files, calculate current raw-file hashes and write the report. The platform will execute independent checks after this turn. Do not claim those checks already passed.',
|
|
@@ -207,7 +217,7 @@ export async function verifyParameterAgentTask(state, operation, payload, root,
|
|
|
207
217
|
const document = await readWorkspaceParameters(root, state.journal.fileRevisions(revisionScope(state, operation.workspaceId, owner.directoryName)));
|
|
208
218
|
if (document.status !== 'ready')
|
|
209
219
|
throw new ParameterAgentError('parameter_agent_parameters_not_ready');
|
|
210
|
-
const catalog =
|
|
220
|
+
const catalog = parameterI18nCatalog(document, 'en');
|
|
211
221
|
const implemented = report.candidates.filter(candidate => candidate.status === 'implemented');
|
|
212
222
|
if (!implemented.length || report.candidates.some(candidate => candidate.status !== 'implemented' || candidate.missing.length))
|
|
213
223
|
throw new ParameterAgentError('parameter_agent_integration_incomplete');
|
|
@@ -220,8 +230,14 @@ export async function verifyParameterAgentTask(state, operation, payload, root,
|
|
|
220
230
|
const field = catalog.find(entry => candidate.fieldPath && entry.key === candidate.fieldPath.join('.'));
|
|
221
231
|
if (!field || !field.editable || !field.canReset)
|
|
222
232
|
throw new ParameterAgentError('parameter_agent_field_not_ready');
|
|
223
|
-
|
|
233
|
+
const invalidValue = field.kind === 'i18n'
|
|
234
|
+
? validateParameterI18nChange(document, { fieldPath: field.fieldPath, locale: 'en', value: String(field.value ?? '') })
|
|
235
|
+
: field.hasOverride && validateParameterChange(document.schema, document.data, { fieldPath: field.fieldPath, value: field.value });
|
|
236
|
+
if (invalidValue)
|
|
224
237
|
throw new ParameterAgentError('parameter_agent_field_not_ready');
|
|
238
|
+
if (field.kind === 'i18n' && document.runtimeI18n &&
|
|
239
|
+
(!evidence.has(document.runtimeI18n.globalPath) || (document.runtimeI18n.themeHash && !evidence.has(document.runtimeI18n.themePath))))
|
|
240
|
+
throw new ParameterAgentError('parameter_agent_evidence_missing');
|
|
225
241
|
if (!candidate.consumers.length)
|
|
226
242
|
throw new ParameterAgentError('parameter_agent_consumer_missing');
|
|
227
243
|
}
|
|
@@ -230,11 +246,17 @@ export async function verifyParameterAgentTask(state, operation, payload, root,
|
|
|
230
246
|
if (typeof packageData.scripts?.[script] !== 'string' || !packageData.scripts[script].trim())
|
|
231
247
|
throw new ParameterAgentError('parameter_agent_check_script_missing');
|
|
232
248
|
}
|
|
249
|
+
const initialization = task.request.action === 'initialize'
|
|
250
|
+
? await inspectParameterInitialization(root, document, report) : undefined;
|
|
233
251
|
task.instanceToken = state.instanceToken;
|
|
234
|
-
await update(state, task, { status: 'checking', checks: [], error: undefined, receiptId: undefined, sourceDigest: before.digest }, root);
|
|
252
|
+
await update(state, task, { status: 'checking', checks: [], error: undefined, receiptId: undefined, productionCheck: undefined, sourceDigest: before.digest }, root);
|
|
235
253
|
const scripts = ['test:parameters', 'build', ...(typeof packageData.scripts?.typecheck === 'string' ? ['typecheck'] : [])];
|
|
254
|
+
let productionPlan;
|
|
255
|
+
let productionEvidence;
|
|
236
256
|
for (const script of scripts) {
|
|
237
257
|
assertActive();
|
|
258
|
+
if (initialization && script === 'build')
|
|
259
|
+
productionPlan = await prepareParameterProductionCheck(root);
|
|
238
260
|
const controller = new AbortController();
|
|
239
261
|
const abort = () => controller.abort();
|
|
240
262
|
options.signal?.addEventListener('abort', abort, { once: true });
|
|
@@ -269,16 +291,23 @@ export async function verifyParameterAgentTask(state, operation, payload, root,
|
|
|
269
291
|
await writePrivateParameterTask(state, task, root);
|
|
270
292
|
if (outcome.code !== 0 || outcome.timedOut || outcome.canceled)
|
|
271
293
|
throw new ParameterAgentError(outcome.timedOut ? 'parameter_agent_check_timeout' : outcome.canceled ? 'parameter_agent_check_canceled' : 'parameter_agent_check_failed');
|
|
294
|
+
if (productionPlan && script === 'build')
|
|
295
|
+
productionEvidence = await inspectParameterProductionCheck(productionPlan, assertActive);
|
|
272
296
|
}
|
|
273
297
|
assertActive();
|
|
274
298
|
const after = await parameterInputSnapshot(root);
|
|
275
299
|
if (after.digest !== before.digest || parameterDigest(await readParameterAgentFile(root, task.record.reportPath, 1024 * 1024)) !== task.reportHash)
|
|
276
300
|
throw new ParameterAgentError('parameter_agent_inputs_changed');
|
|
277
|
-
|
|
301
|
+
if (initialization && initialization !== await inspectParameterInitialization(root, document, report))
|
|
302
|
+
throw new ParameterAgentError('parameter_agent_inputs_changed');
|
|
303
|
+
if (productionPlan && stableStringify(productionEvidence) !== stableStringify(await inspectParameterProductionCheck(productionPlan, assertActive)))
|
|
304
|
+
throw new ParameterAgentError('parameter_agent_production_outputs_not_ready');
|
|
305
|
+
return update(state, task, { status: 'verified', sourceDigest: after.digest, receiptId: randomUUID(), error: undefined,
|
|
306
|
+
...(productionEvidence ? { productionCheck: { ...productionEvidence, checkedAt: now() } } : {}) }, root);
|
|
278
307
|
}
|
|
279
308
|
catch (cause) {
|
|
280
309
|
const code = errorCode(cause);
|
|
281
310
|
const status = code === 'ENOENT' ? 'missing' : /stale|changed|expired/.test(code) ? 'stale' : /(?:not_ready|incomplete|consumer_missing|script_missing|evidence_missing)$/.test(code) ? 'needs-verification' : /report_|evidence_invalid|path_invalid/.test(code) ? 'invalid' : 'failed';
|
|
282
|
-
return update(state, task, { status, error: code === 'ENOENT' ? 'parameter_agent_report_missing' : code, receiptId: undefined }, root);
|
|
311
|
+
return update(state, task, { status, error: code === 'ENOENT' ? 'parameter_agent_report_missing' : code, receiptId: undefined, productionCheck: undefined }, root);
|
|
283
312
|
}
|
|
284
313
|
}
|
|
@@ -159,6 +159,7 @@ export async function writeParameterAssetRegistration(root, plan) {
|
|
|
159
159
|
throw new ParameterAssetError('parameter_asset_index_conflict');
|
|
160
160
|
const parent = await lstat(dirname(target));
|
|
161
161
|
const bytes = Buffer.from(plan.content);
|
|
162
|
+
// private
|
|
162
163
|
staged = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
163
164
|
// Metadata restoration must succeed before publication; never downgrade to a partial text write.
|
|
164
165
|
await staged.chown(before.uid, before.gid);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { lstat, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
const ownerSchema = z.object({ kind: z.literal('playcraft.parameter-skill.v1'), pid: z.number().int().positive(), token: z.string().uuid() }).strict();
|
|
6
|
+
export const PARAMETER_SKILL_TRANSACTION = 'parameter-initialization-skill.transaction.json';
|
|
7
|
+
const transactionSchema = z.object({ version: z.literal(1), owner: ownerSchema,
|
|
8
|
+
templateHash: z.string().regex(/^[a-f0-9]{64}$/), artifactHash: z.string().regex(/^[a-f0-9]{64}$/), sourceAdded: z.boolean(),
|
|
9
|
+
originalDeclaration: z.string().nullable(), originalLock: z.string().nullable(),
|
|
10
|
+
nextDeclaration: z.string(), nextLock: z.string(),
|
|
11
|
+
}).strict();
|
|
12
|
+
export async function readParameterSkillTransactionFile(path, maximum = 8 * 1024 * 1024) {
|
|
13
|
+
try {
|
|
14
|
+
const stat = await lstat(path);
|
|
15
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > maximum)
|
|
16
|
+
throw new Error('workspace_skills_parameter_recovery_conflict');
|
|
17
|
+
return await readFile(path);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error.code === 'ENOENT')
|
|
21
|
+
return undefined;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function parseOwner(bytes) {
|
|
26
|
+
try {
|
|
27
|
+
return ownerSchema.parse(JSON.parse(bytes.toString('utf8')));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw Object.assign(new Error('workspace_skills_parameter_recovery_conflict'), { code: 'EEXIST' });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function alive(pid) {
|
|
34
|
+
try {
|
|
35
|
+
process.kill(pid, 0);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return error.code !== 'ESRCH';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function removeExact(path, expected) {
|
|
43
|
+
const current = await readParameterSkillTransactionFile(path);
|
|
44
|
+
if (current === undefined || !current.equals(expected))
|
|
45
|
+
throw new Error('workspace_skills_parameter_recovery_conflict');
|
|
46
|
+
await rm(path);
|
|
47
|
+
}
|
|
48
|
+
/** Each dead recovery owner has its own exclusive successor, avoiding an unchecked lease takeover. */
|
|
49
|
+
async function recoveryClaim(management, previous, claims, depth = 0) {
|
|
50
|
+
if (depth > 32)
|
|
51
|
+
throw new Error('workspace_skills_parameter_recovery_conflict');
|
|
52
|
+
const path = join(management, `parameter-initialization-${previous.token}.recovery.lock`);
|
|
53
|
+
const owner = { kind: 'playcraft.parameter-skill.v1', pid: process.pid, token: randomUUID() };
|
|
54
|
+
const bytes = Buffer.from(JSON.stringify(owner));
|
|
55
|
+
// private: lease identities and crash recovery records belong to the controller.
|
|
56
|
+
try {
|
|
57
|
+
await writeFile(path, bytes, { flag: 'wx', mode: 0o600 });
|
|
58
|
+
claims.push({ path, bytes });
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code !== 'EEXIST')
|
|
62
|
+
throw error;
|
|
63
|
+
const existing = await readParameterSkillTransactionFile(path, 4096);
|
|
64
|
+
if (!existing)
|
|
65
|
+
throw error;
|
|
66
|
+
const held = parseOwner(existing);
|
|
67
|
+
if (alive(held.pid))
|
|
68
|
+
throw error;
|
|
69
|
+
claims.push({ path, bytes: existing });
|
|
70
|
+
await recoveryClaim(management, held, claims, depth + 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export async function acquireParameterSkillLease(management, recover) {
|
|
74
|
+
const path = join(management, 'skills-write.lock');
|
|
75
|
+
const owner = { kind: 'playcraft.parameter-skill.v1', pid: process.pid, token: randomUUID() };
|
|
76
|
+
const bytes = Buffer.from(JSON.stringify(owner));
|
|
77
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
78
|
+
try {
|
|
79
|
+
// private
|
|
80
|
+
await writeFile(path, bytes, { flag: 'wx', mode: 0o600 });
|
|
81
|
+
return { owner, release: () => removeExact(path, bytes) };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code !== 'EEXIST')
|
|
85
|
+
throw error;
|
|
86
|
+
const previousBytes = await readParameterSkillTransactionFile(path, 4096);
|
|
87
|
+
if (!previousBytes)
|
|
88
|
+
throw error;
|
|
89
|
+
const previous = parseOwner(previousBytes);
|
|
90
|
+
if (alive(previous.pid))
|
|
91
|
+
throw error;
|
|
92
|
+
const claims = [];
|
|
93
|
+
let claimed = false;
|
|
94
|
+
try {
|
|
95
|
+
await recoveryClaim(management, previous, claims);
|
|
96
|
+
claimed = true;
|
|
97
|
+
const current = await readParameterSkillTransactionFile(path, 4096);
|
|
98
|
+
if (!current?.equals(previousBytes) || alive(previous.pid))
|
|
99
|
+
throw new Error('workspace_skills_parameter_recovery_conflict');
|
|
100
|
+
const marker = join(management, PARAMETER_SKILL_TRANSACTION);
|
|
101
|
+
const transactionBytes = await readParameterSkillTransactionFile(marker);
|
|
102
|
+
if (transactionBytes) {
|
|
103
|
+
let transaction;
|
|
104
|
+
try {
|
|
105
|
+
transaction = transactionSchema.parse(JSON.parse(transactionBytes.toString('utf8')));
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new Error('workspace_skills_parameter_recovery_conflict');
|
|
109
|
+
}
|
|
110
|
+
if (transaction.owner.pid !== previous.pid || transaction.owner.token !== previous.token)
|
|
111
|
+
throw new Error('workspace_skills_parameter_recovery_conflict');
|
|
112
|
+
await recover(transaction);
|
|
113
|
+
await removeExact(marker, transactionBytes);
|
|
114
|
+
}
|
|
115
|
+
await removeExact(path, previousBytes);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
if (claimed)
|
|
119
|
+
for (const claim of claims.reverse())
|
|
120
|
+
await removeExact(claim.path, claim.bytes);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw new Error('workspace_skills_parameter_recovery_conflict');
|
|
125
|
+
}
|
|
126
|
+
export async function writeParameterSkillTransaction(management, transaction) {
|
|
127
|
+
const path = join(management, PARAMETER_SKILL_TRANSACTION);
|
|
128
|
+
const bytes = Buffer.from(JSON.stringify(transactionSchema.parse(transaction)));
|
|
129
|
+
if (await readParameterSkillTransactionFile(path))
|
|
130
|
+
throw new Error('workspace_skills_parameter_recovery_conflict');
|
|
131
|
+
const temporary = `${path}.${transaction.owner.token}.tmp`;
|
|
132
|
+
try {
|
|
133
|
+
// private
|
|
134
|
+
await writeFile(temporary, bytes, { flag: 'wx', mode: 0o600 });
|
|
135
|
+
await rename(temporary, path);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
await rm(temporary, { force: true });
|
|
139
|
+
}
|
|
140
|
+
return () => removeExact(path, bytes);
|
|
141
|
+
}
|