@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.
Files changed (89) hide show
  1. package/dist/commands/workspace.js +43 -0
  2. package/dist/utils/agent-generation-provenance.js +1 -1
  3. package/dist/workspace/messages.js +10 -0
  4. package/dist/workspace-runtime/batch/deliver.js +140 -80
  5. package/dist/workspace-runtime/batch/freeze.js +18 -6
  6. package/dist/workspace-runtime/batch/generate.js +68 -64
  7. package/dist/workspace-runtime/batch/policy.js +71 -22
  8. package/dist/workspace-runtime/batch/prepare.js +108 -93
  9. package/dist/workspace-runtime/batch/records.js +4 -0
  10. package/dist/workspace-runtime/batch/routes.js +10 -2
  11. package/dist/workspace-runtime/batch/source-candidate.js +2 -0
  12. package/dist/workspace-runtime/batch/source-capture.js +1 -0
  13. package/dist/workspace-runtime/batch/source-failure.js +2 -0
  14. package/dist/workspace-runtime/batch/source-publish.js +3 -0
  15. package/dist/workspace-runtime/batch/writer-proof.js +37 -0
  16. package/dist/workspace-runtime/codex/app-server.js +278 -20
  17. package/dist/workspace-runtime/codex/config-toml.js +4 -0
  18. package/dist/workspace-runtime/codex/delivery-tools.js +1 -1
  19. package/dist/workspace-runtime/codex/generation-provenance-broker.js +17 -8
  20. package/dist/workspace-runtime/codex/loopback.js +2 -0
  21. package/dist/workspace-runtime/codex/model-broker.js +7 -4
  22. package/dist/workspace-runtime/persistence/journal.js +34 -0
  23. package/dist/workspace-runtime/persistence/sqlite-worker.js +68 -6
  24. package/dist/workspace-runtime/previews/output.js +2 -0
  25. package/dist/workspace-runtime/previews/publish.js +20 -5
  26. package/dist/workspace-runtime/previews/runtime-preview.js +4 -0
  27. package/dist/workspace-runtime/processes/managed-writes.js +47 -5
  28. package/dist/workspace-runtime/serve.js +13 -3
  29. package/dist/workspace-runtime/server/accept-operation.js +22 -2
  30. package/dist/workspace-runtime/server/directory-access.js +414 -0
  31. package/dist/workspace-runtime/server/dispatch.js +74 -29
  32. package/dist/workspace-runtime/server/endpoints.js +1 -0
  33. package/dist/workspace-runtime/server/http.js +80 -10
  34. package/dist/workspace-runtime/server/operation-record.js +2 -0
  35. package/dist/workspace-runtime/server/terminal-host.js +124 -8
  36. package/dist/workspace-runtime/server/workspace-storage.js +193 -0
  37. package/dist/workspace-runtime/server/workspace-transfer.js +3 -1
  38. package/dist/workspace-runtime/workspaces/append-only-log.js +3 -0
  39. package/dist/workspace-runtime/workspaces/artifact-events.js +5 -2
  40. package/dist/workspace-runtime/workspaces/artifact-files.js +14 -8
  41. package/dist/workspace-runtime/workspaces/artifact-receipts.js +7 -25
  42. package/dist/workspace-runtime/workspaces/artifacts.js +4 -1
  43. package/dist/workspace-runtime/workspaces/asset-library-import.js +9 -4
  44. package/dist/workspace-runtime/workspaces/asset-publication-access.js +7 -9
  45. package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +3 -0
  46. package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +17 -2
  47. package/dist/workspace-runtime/workspaces/attachments.js +9 -0
  48. package/dist/workspace-runtime/workspaces/batch-commit.js +5 -3
  49. package/dist/workspace-runtime/workspaces/binary-write.js +12 -7
  50. package/dist/workspace-runtime/workspaces/cli-context.js +2 -0
  51. package/dist/workspace-runtime/workspaces/cli-credentials.js +1 -0
  52. package/dist/workspace-runtime/workspaces/delivery-build.js +30 -11
  53. package/dist/workspace-runtime/workspaces/delivery-store.js +2 -0
  54. package/dist/workspace-runtime/workspaces/dependencies.js +90 -70
  55. package/dist/workspace-runtime/workspaces/developer-command.js +12 -1
  56. package/dist/workspace-runtime/workspaces/directory-access.js +13 -18
  57. package/dist/workspace-runtime/workspaces/directory-attachments.js +8 -0
  58. package/dist/workspace-runtime/workspaces/directory-create.js +18 -5
  59. package/dist/workspace-runtime/workspaces/directory-maintenance.js +46 -35
  60. package/dist/workspace-runtime/workspaces/directory-mutations.js +8 -3
  61. package/dist/workspace-runtime/workspaces/files.js +4 -0
  62. package/dist/workspace-runtime/workspaces/git-credential-broker.js +3 -0
  63. package/dist/workspace-runtime/workspaces/git-direct-store.js +2 -0
  64. package/dist/workspace-runtime/workspaces/json5-edit.js +36 -0
  65. package/dist/workspace-runtime/workspaces/library-attachments.js +4 -0
  66. package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +2 -0
  67. package/dist/workspace-runtime/workspaces/parameter-agent-store.js +2 -0
  68. package/dist/workspace-runtime/workspaces/parameter-agent.js +40 -11
  69. package/dist/workspace-runtime/workspaces/parameter-assets.js +1 -0
  70. package/dist/workspace-runtime/workspaces/parameter-initialization-skill-transaction.js +141 -0
  71. package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +329 -0
  72. package/dist/workspace-runtime/workspaces/parameter-initialization.js +63 -0
  73. package/dist/workspace-runtime/workspaces/parameter-production.js +124 -0
  74. package/dist/workspace-runtime/workspaces/parameters.js +95 -3
  75. package/dist/workspace-runtime/workspaces/platform-export.js +5 -0
  76. package/dist/workspace-runtime/workspaces/prepare.js +4 -0
  77. package/dist/workspace-runtime/workspaces/registry.js +55 -3
  78. package/dist/workspace-runtime/workspaces/remix-references.js +5 -0
  79. package/dist/workspace-runtime/workspaces/shared-write.js +187 -0
  80. package/dist/workspace-runtime/workspaces/skills.js +14 -15
  81. package/dist/workspace-runtime/workspaces/storage-snapshot.js +235 -0
  82. package/dist/workspace-runtime/workspaces/transfer.js +4 -1
  83. package/dist/workspace-runtime/workspaces/workspace-shares.js +235 -0
  84. package/dist/workspace-runtime/workspaces/worktree-attachments.js +2 -0
  85. package/dist/workspace-runtime/workspaces/worktree-checkout.js +11 -6
  86. package/package.json +5 -4
  87. package/project-template/.claude/skills/playcraft-runtime-parameters/SKILL.md +32 -0
  88. package/project-template/.claude/skills/playcraft-runtime-parameters/references/platform-protocol.md +21 -0
  89. 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, parameterCatalog, stableStringify, validateParameterChange, } from '@playcraft/common/cloud-workspace/runtime';
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
- PARAMETER_AGENT_PROTOCOL,
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 = parameterCatalog(document.schema, document.data);
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
- if (field.hasOverride && validateParameterChange(document.schema, document.data, { fieldPath: field.fieldPath, value: field.value }))
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
- return update(state, task, { status: 'verified', sourceDigest: after.digest, receiptId: randomUUID(), error: undefined }, root);
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
+ }