@prismer/runtime 2.0.8 → 2.2.55

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 (119) hide show
  1. package/CHANGELOG.md +3430 -0
  2. package/README.md +34 -12
  3. package/apc/skills/FIELD-DICTIONARY.md +111 -0
  4. package/apc/skills/bug-reproduce/SKILL.md +150 -0
  5. package/apc/skills/bug-reproduce/skill.json +96 -0
  6. package/apc/skills/code-review/SKILL.md +198 -0
  7. package/apc/skills/code-review/skill.json +124 -0
  8. package/apc/skills/design-review/SKILL.md +122 -0
  9. package/apc/skills/design-review/skill.json +88 -0
  10. package/apc/skills/doc-sync/SKILL.md +168 -0
  11. package/apc/skills/doc-sync/skill.json +81 -0
  12. package/apc/skills/env-doctor/SKILL.md +194 -0
  13. package/apc/skills/env-doctor/skill.json +209 -0
  14. package/apc/skills/git-ops/SKILL.md +189 -0
  15. package/apc/skills/git-ops/skill.json +94 -0
  16. package/apc/skills/impact-trace/SKILL.md +168 -0
  17. package/apc/skills/impact-trace/skill.json +104 -0
  18. package/apc/skills/observability/SKILL.md +195 -0
  19. package/apc/skills/observability/skill.json +116 -0
  20. package/apc/skills/release-db-config-sync/SKILL.md +186 -0
  21. package/apc/skills/release-db-config-sync/skill.json +109 -0
  22. package/apc/skills/release-ota-promote/SKILL.md +195 -0
  23. package/apc/skills/release-ota-promote/skill.json +176 -0
  24. package/apc/skills/release-preflight/SKILL.md +174 -0
  25. package/apc/skills/release-preflight/skill.json +175 -0
  26. package/apc/skills/release-rollback/SKILL.md +214 -0
  27. package/apc/skills/release-rollback/skill.json +230 -0
  28. package/apc/skills/release-tag/SKILL.md +194 -0
  29. package/apc/skills/release-tag/skill.json +94 -0
  30. package/apc/skills/releasing-prod/SKILL.md +49 -0
  31. package/apc/skills/releasing-test/SKILL.md +135 -0
  32. package/apc/skills/sdk-release/SKILL.md +200 -0
  33. package/apc/skills/spec-intake/SKILL.md +169 -0
  34. package/apc/skills/spec-intake/skill.json +93 -0
  35. package/apc/skills/test-result-feedback/SKILL.md +239 -0
  36. package/apc/skills/test-result-feedback/skill.json +193 -0
  37. package/apc/skills/test-runner/SKILL.md +169 -0
  38. package/apc/skills/test-runner/skill.json +103 -0
  39. package/apc/skills/ui-align/SKILL.md +209 -0
  40. package/apc/skills/ui-align/skill.json +114 -0
  41. package/apc/skills/ui-canvas/SKILL.md +148 -0
  42. package/apc/skills/ui-canvas/skill.json +127 -0
  43. package/built-in-skills/agent-coordination/SKILL.md +59 -37
  44. package/built-in-skills/agent-meta/SKILL.md +1 -0
  45. package/built-in-skills/assets/SKILL.md +8 -6
  46. package/built-in-skills/browser-use/SKILL.md +93 -0
  47. package/built-in-skills/canvas-design/SKILL.md +1 -0
  48. package/built-in-skills/claim-agent-ownership/SKILL.md +3 -2
  49. package/built-in-skills/claude-api/SKILL.md +1 -0
  50. package/built-in-skills/codebase-design/DEEPENING.md +37 -0
  51. package/built-in-skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  52. package/built-in-skills/codebase-design/LICENSE +21 -0
  53. package/built-in-skills/codebase-design/SKILL.md +116 -0
  54. package/built-in-skills/conversation-compaction/SKILL.md +114 -0
  55. package/built-in-skills/council-creator/SKILL.md +426 -0
  56. package/built-in-skills/diagnosing-bugs/LICENSE +21 -0
  57. package/built-in-skills/diagnosing-bugs/SKILL.md +136 -0
  58. package/built-in-skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  59. package/built-in-skills/doc-coauthoring/SKILL.md +1 -0
  60. package/built-in-skills/document-generation/SKILL.md +105 -0
  61. package/built-in-skills/domain-modeling/ADR-FORMAT.md +47 -0
  62. package/built-in-skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  63. package/built-in-skills/domain-modeling/LICENSE +21 -0
  64. package/built-in-skills/domain-modeling/SKILL.md +76 -0
  65. package/built-in-skills/frontend-design/SKILL.md +1 -0
  66. package/built-in-skills/human-approval/SKILL.md +17 -2
  67. package/built-in-skills/image-generate/SKILL.md +103 -302
  68. package/built-in-skills/image-generate/scripts/generate-and-deliver.mjs +289 -0
  69. package/built-in-skills/ingest/SKILL.md +13 -45
  70. package/built-in-skills/internal-comms/SKILL.md +1 -0
  71. package/built-in-skills/liteparse/SKILL.md +130 -110
  72. package/built-in-skills/mcp-builder/SKILL.md +1 -0
  73. package/built-in-skills/memory/SKILL.md +420 -55
  74. package/built-in-skills/memory-dream/SKILL.md +339 -0
  75. package/built-in-skills/office-artifacts/SKILL.md +17 -4
  76. package/built-in-skills/okr/SKILL.md +154 -0
  77. package/built-in-skills/persona/SKILL.md +81 -0
  78. package/built-in-skills/persona-generator/SKILL.md +296 -0
  79. package/built-in-skills/pkf-svg/SKILL.md +253 -0
  80. package/built-in-skills/pkf-writing/SKILL.md +236 -0
  81. package/built-in-skills/prismer-im-collab/SKILL.md +26 -6
  82. package/built-in-skills/proactivity/SKILL.md +84 -0
  83. package/built-in-skills/remotion/SKILL.md +431 -0
  84. package/built-in-skills/role-builder/SKILL.md +203 -0
  85. package/built-in-skills/role-builder/scripts/author-role.mjs +334 -0
  86. package/built-in-skills/role-builder/scripts/ingest-role.mjs +223 -0
  87. package/built-in-skills/role-builder/scripts/instantiate-and-run.mjs +290 -0
  88. package/built-in-skills/role-builder/scripts/operation-harness.mjs +267 -0
  89. package/built-in-skills/skill-authoring/SKILL.md +110 -100
  90. package/built-in-skills/skill-authoring/skill.json +3 -3
  91. package/built-in-skills/skill-builder/SKILL.md +171 -0
  92. package/built-in-skills/skill-builder/scripts/ingest.mjs +265 -0
  93. package/built-in-skills/skill-creator/SKILL.md +165 -423
  94. package/built-in-skills/skill-creator/references/external-library-import.md +110 -0
  95. package/built-in-skills/skill-creator/scripts/import-library.mjs +475 -0
  96. package/built-in-skills/slack-gif-creator/SKILL.md +20 -0
  97. package/built-in-skills/tasks/SKILL.md +38 -23
  98. package/built-in-skills/tdd/LICENSE +21 -0
  99. package/built-in-skills/tdd/SKILL.md +110 -0
  100. package/built-in-skills/tdd/mocking.md +59 -0
  101. package/built-in-skills/tdd/refactoring.md +10 -0
  102. package/built-in-skills/tdd/tests.md +61 -0
  103. package/built-in-skills/team/SKILL.md +2 -1
  104. package/built-in-skills/web-artifacts-builder/SKILL.md +1 -0
  105. package/built-in-skills/webapp-testing/SKILL.md +1 -0
  106. package/built-in-skills/wechat-pay/SKILL.md +59 -0
  107. package/dist/cli.cjs +71872 -19960
  108. package/dist/cli.js +71803 -19846
  109. package/dist/index.cjs +72010 -19966
  110. package/dist/index.d.cts +4258 -712
  111. package/dist/index.d.ts +4258 -712
  112. package/dist/index.js +72156 -20118
  113. package/package.json +37 -6
  114. package/plugins/memory/prismer/__init__.py +1211 -0
  115. package/plugins/memory/prismer/plugin.yaml +8 -0
  116. package/plugins/memory/prismer/tool-schemas.generated.json +249 -0
  117. package/plugins/tools/prismer-recall/__init__.py +282 -0
  118. package/plugins/tools/prismer-recall/plugin.yaml +15 -0
  119. package/built-in-skills/memory-curation/SKILL.md +0 -135
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ HarnessError,
5
+ invocationHash,
6
+ loadHarnessConfig,
7
+ readLedger,
8
+ requestJson,
9
+ sleep,
10
+ taskIdempotencyKey,
11
+ writeLedger,
12
+ } from './operation-harness.mjs';
13
+
14
+ const HELP = `Usage:
15
+ # with the required PRISMER_* environment already injected:
16
+ node scripts/instantiate-and-run.mjs [--json]
17
+
18
+ Configuration stays in environment variables. Non-secret flags are available:
19
+ --role --workspace --task --title --handle --display-name --adapter
20
+ --target-daemon --request-id --ledger --timeout-ms --poll-ms
21
+ --preflight-only --no-wait --json
22
+
23
+ Remote writes require PRISMER_ALLOW_REMOTE_WRITE=1 from the trusted environment.
24
+ Set PRISMER_REQUEST_ID once when an interrupted operation must be resumed.
25
+
26
+ Exit codes: 2 local harness rejection, 3 auth, 4 transient/network, 5 remote contract.`;
27
+
28
+ function instanceBody(config) {
29
+ return {
30
+ workspaceId: config.workspaceId,
31
+ requestId: config.requestId,
32
+ ...(config.handle ? { handle: config.handle } : {}),
33
+ ...(config.displayName ? { displayName: config.displayName } : {}),
34
+ adapterName: config.adapterName,
35
+ ...(config.targetDaemonId ? { targetDaemonId: config.targetDaemonId } : {}),
36
+ };
37
+ }
38
+
39
+ function emit(config, ledger) {
40
+ if (config.json) process.stdout.write(`${JSON.stringify(ledger)}\n`);
41
+ else {
42
+ process.stdout.write(
43
+ `[role-builder] ${ledger.stage}: role=${config.role} workspace=${config.workspaceId}` +
44
+ `${ledger.agent?.id ? ` agent=${ledger.agent.id}` : ''}${ledger.task?.id ? ` task=${ledger.task.id}` : ''}\n`,
45
+ );
46
+ }
47
+ }
48
+
49
+ async function findExistingInstance(config) {
50
+ try {
51
+ const read = await requestJson(
52
+ config,
53
+ 'GET',
54
+ `/api/im/role-templates/${encodeURIComponent(config.role)}/instances/${encodeURIComponent(config.requestId)}`,
55
+ undefined,
56
+ 'operation_lookup',
57
+ );
58
+ return read.data;
59
+ } catch (error) {
60
+ if (error instanceof HarnessError && error.details?.status === 404) return null;
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ async function waitForInstance(config, initial, ledger) {
66
+ let instance = initial;
67
+ const started = Date.now();
68
+ let lastResumeAt = started;
69
+ while (Date.now() - started < config.timeoutMs) {
70
+ if (instance.status === 'failed') {
71
+ throw new HarnessError(
72
+ 'instance',
73
+ instance.error?.code || 'ROLE_INSTANCE_FAILED',
74
+ instance.error?.message || 'role instance provisioning failed',
75
+ instance.error?.retryable ? 4 : 5,
76
+ );
77
+ }
78
+ if (instance.execution?.state === 'wrong_daemon') {
79
+ throw new HarnessError(
80
+ 'instance_wait',
81
+ 'ROLE_INSTANCE_WRONG_DAEMON',
82
+ `agent is bound to ${instance.execution.boundDaemonId || 'an unknown daemon'} instead of ${instance.execution.targetDaemonId || 'the requested daemon'}`,
83
+ 5,
84
+ {
85
+ targetDaemonId: instance.execution.targetDaemonId ?? null,
86
+ boundDaemonId: instance.execution.boundDaemonId ?? null,
87
+ },
88
+ );
89
+ }
90
+ // A task must never be created for an agent that the runtime has not yet
91
+ // adopted. `--no-wait` skips only the final task-result wait.
92
+ if (instance.status === 'ready' && instance.execution?.state === 'ready') return instance;
93
+ await sleep(config.pollMs);
94
+ const read = await requestJson(
95
+ config,
96
+ 'GET',
97
+ `/api/im/role-templates/${encodeURIComponent(config.role)}/instances/${encodeURIComponent(config.requestId)}`,
98
+ undefined,
99
+ 'instance_wait',
100
+ );
101
+ instance = read.data;
102
+ ledger.instance = instance;
103
+ ledger.stage = instance.status === 'ready' ? 'waiting_for_runtime' : 'provisioning_agent';
104
+ ledger.updatedAt = new Date().toISOString();
105
+ writeLedger(config.ledgerPath, ledger);
106
+
107
+ // A process may die after accepting the operation. Periodically re-POSTing
108
+ // is safe: the server lease decides whether this caller may resume it.
109
+ if (instance.status === 'provisioning' && Date.now() - lastResumeAt >= 35_000) {
110
+ const resumed = await requestJson(
111
+ config,
112
+ 'POST',
113
+ `/api/im/role-templates/${encodeURIComponent(config.role)}/instances`,
114
+ instanceBody(config),
115
+ 'instance_resume',
116
+ );
117
+ instance = resumed.data;
118
+ lastResumeAt = Date.now();
119
+ }
120
+ }
121
+ throw new HarnessError('instance_wait', 'WORKFLOW_TIMEOUT', 'timed out waiting for agent runtime binding', 4);
122
+ }
123
+
124
+ async function waitForTask(config, task, ledger) {
125
+ const started = Date.now();
126
+ while (Date.now() - started < config.timeoutMs) {
127
+ if (['completed', 'failed', 'cancelled'].includes(task.status)) return task;
128
+ await sleep(config.pollMs);
129
+ const read = await requestJson(
130
+ config,
131
+ 'GET',
132
+ `/api/im/tasks/${encodeURIComponent(task.id)}`,
133
+ undefined,
134
+ 'task_wait',
135
+ );
136
+ task = read.data;
137
+ ledger.task = task;
138
+ ledger.stage = 'waiting_for_task';
139
+ ledger.updatedAt = new Date().toISOString();
140
+ writeLedger(config.ledgerPath, ledger);
141
+ }
142
+ throw new HarnessError('task_wait', 'WORKFLOW_TIMEOUT', 'timed out waiting for task result', 4);
143
+ }
144
+
145
+ export async function runRoleWorkflow(config) {
146
+ const acceptedHash = invocationHash(config);
147
+ const previous = readLedger(config.ledgerPath);
148
+ if (
149
+ previous &&
150
+ (previous.requestId !== config.requestId ||
151
+ previous.role !== config.role ||
152
+ previous.workspaceId !== config.workspaceId ||
153
+ (previous.invocationHash && previous.invocationHash !== acceptedHash))
154
+ ) {
155
+ throw new HarnessError(
156
+ 'local_validation',
157
+ 'OPERATION_LEDGER_CONFLICT',
158
+ 'the ledger path belongs to a different role workflow request',
159
+ 2,
160
+ );
161
+ }
162
+ const ledger = {
163
+ schemaVersion: 2,
164
+ requestId: config.requestId,
165
+ invocationHash: acceptedHash,
166
+ role: config.role,
167
+ workspaceId: config.workspaceId,
168
+ stage: 'local_validation_complete',
169
+ startedAt: new Date().toISOString(),
170
+ updatedAt: new Date().toISOString(),
171
+ };
172
+ // Persist the recovery key before the first request. Credentials are never
173
+ // written to the ledger or included in the result.
174
+ writeLedger(config.ledgerPath, ledger);
175
+
176
+ const existing = config.preflightOnly ? null : await findExistingInstance(config);
177
+ if (!existing || config.preflightOnly) {
178
+ const preflight = await requestJson(
179
+ config,
180
+ 'POST',
181
+ `/api/im/role-templates/${encodeURIComponent(config.role)}/instances/preflight`,
182
+ instanceBody(config),
183
+ 'remote_preflight',
184
+ );
185
+ ledger.preflight = preflight.data;
186
+ ledger.stage = 'remote_preflight_complete';
187
+ ledger.updatedAt = new Date().toISOString();
188
+ writeLedger(config.ledgerPath, ledger);
189
+ if (config.preflightOnly) return ledger;
190
+ } else {
191
+ ledger.instance = existing;
192
+ ledger.stage = 'operation_recovered';
193
+ ledger.updatedAt = new Date().toISOString();
194
+ writeLedger(config.ledgerPath, ledger);
195
+ }
196
+
197
+ // A ready/provisioning durable operation can be resumed from its read face.
198
+ // A failed operation needs an idempotent POST to re-enter server recovery.
199
+ const provisioned =
200
+ existing && existing.status !== 'failed'
201
+ ? { data: existing }
202
+ : await requestJson(
203
+ config,
204
+ 'POST',
205
+ `/api/im/role-templates/${encodeURIComponent(config.role)}/instances`,
206
+ instanceBody(config),
207
+ 'instance',
208
+ );
209
+ ledger.instance = provisioned.data;
210
+ ledger.stage = 'provisioning_agent';
211
+ ledger.updatedAt = new Date().toISOString();
212
+ writeLedger(config.ledgerPath, ledger);
213
+ const instance = await waitForInstance(config, provisioned.data, ledger);
214
+ ledger.instance = instance;
215
+ ledger.agent = instance.agent;
216
+ ledger.stage = config.wait ? 'agent_ready' : 'agent_created';
217
+ ledger.updatedAt = new Date().toISOString();
218
+ writeLedger(config.ledgerPath, ledger);
219
+
220
+ const taskRequestKey = taskIdempotencyKey(config.requestId);
221
+ const createdTask = await requestJson(
222
+ config,
223
+ 'POST',
224
+ '/api/im/tasks',
225
+ {
226
+ title: config.title,
227
+ description: config.task,
228
+ workspaceId: config.workspaceId,
229
+ assigneeId: instance.agent.id,
230
+ runtimeRoute: 'agent',
231
+ dispatchPolicy: 'on-assign',
232
+ idempotencyKey: taskRequestKey,
233
+ metadata: {
234
+ kind: 'agent_run',
235
+ createdVia: 'role-builder-script',
236
+ roleInstanceRequestId: config.requestId,
237
+ },
238
+ },
239
+ 'task_create',
240
+ );
241
+ ledger.task = createdTask.data;
242
+ ledger.stage = 'task_created';
243
+ ledger.updatedAt = new Date().toISOString();
244
+ writeLedger(config.ledgerPath, ledger);
245
+
246
+ if (config.wait) {
247
+ ledger.task = await waitForTask(config, createdTask.data, ledger);
248
+ if (ledger.task.status !== 'completed') {
249
+ throw new HarnessError(
250
+ 'task_wait',
251
+ `TASK_${String(ledger.task.status).toUpperCase()}`,
252
+ ledger.task.error || `task ended as ${ledger.task.status}`,
253
+ 5,
254
+ );
255
+ }
256
+ }
257
+ ledger.stage = config.wait ? 'completed' : 'dispatched';
258
+ ledger.completedAt = new Date().toISOString();
259
+ ledger.updatedAt = ledger.completedAt;
260
+ writeLedger(config.ledgerPath, ledger);
261
+ return ledger;
262
+ }
263
+
264
+ async function main() {
265
+ let config;
266
+ try {
267
+ config = loadHarnessConfig(process.argv.slice(2));
268
+ if (config.help) {
269
+ process.stdout.write(`${HELP}\n`);
270
+ return;
271
+ }
272
+ const result = await runRoleWorkflow(config);
273
+ emit(config, result);
274
+ } catch (error) {
275
+ const harnessError =
276
+ error instanceof HarnessError
277
+ ? error
278
+ : new HarnessError('internal', 'UNEXPECTED_ERROR', error?.message || String(error), 1);
279
+ const failure = {
280
+ ok: false,
281
+ stage: harnessError.stage,
282
+ error: { code: harnessError.code, message: harnessError.message, details: harnessError.details },
283
+ ...(config ? { requestId: config.requestId, ledger: config.ledgerPath } : {}),
284
+ };
285
+ process.stderr.write(`${JSON.stringify(failure)}\n`);
286
+ process.exitCode = harnessError.exitCode;
287
+ }
288
+ }
289
+
290
+ if (import.meta.url === `file://${process.argv[1]}`) await main();
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
5
+ import { dirname, resolve } from 'node:path';
6
+
7
+ export class HarnessError extends Error {
8
+ constructor(stage, code, message, exitCode = 1, details = undefined) {
9
+ super(message);
10
+ this.name = 'HarnessError';
11
+ this.stage = stage;
12
+ this.code = code;
13
+ this.exitCode = exitCode;
14
+ this.details = details;
15
+ }
16
+ }
17
+
18
+ const VALUE_OPTIONS = new Set([
19
+ '--role',
20
+ '--workspace',
21
+ '--task',
22
+ '--title',
23
+ '--handle',
24
+ '--display-name',
25
+ '--adapter',
26
+ '--target-daemon',
27
+ '--request-id',
28
+ '--timeout-ms',
29
+ '--poll-ms',
30
+ '--ledger',
31
+ ]);
32
+ const BOOLEAN_OPTIONS = new Set(['--json', '--no-wait', '--preflight-only', '--help']);
33
+ const SECRET_OPTIONS = new Set(['--api-key', '--token', '--jwt', '--secret', '--password']);
34
+
35
+ function optionName(arg) {
36
+ if (typeof arg !== 'string') return '';
37
+ const equalsAt = arg.indexOf('=');
38
+ return equalsAt === -1 ? arg : arg.slice(0, equalsAt);
39
+ }
40
+
41
+ export function parseHarnessArgs(argv) {
42
+ const values = {};
43
+ const flags = new Set();
44
+ for (let index = 0; index < argv.length; index += 1) {
45
+ const arg = argv[index];
46
+ const name = optionName(arg);
47
+ if (SECRET_OPTIONS.has(name)) {
48
+ throw new HarnessError(
49
+ 'local_validation',
50
+ 'SECRET_ARGUMENT_FORBIDDEN',
51
+ `${name} is forbidden; credentials must come from environment variables`,
52
+ 2,
53
+ );
54
+ }
55
+ if (BOOLEAN_OPTIONS.has(arg)) {
56
+ flags.add(arg);
57
+ continue;
58
+ }
59
+ if (!VALUE_OPTIONS.has(arg)) {
60
+ // Never reflect an untrusted argv token. Besides ordinary typos, it may
61
+ // contain a credential in an unrecognised --name=value form.
62
+ throw new HarnessError('local_validation', 'UNKNOWN_ARGUMENT', `unknown argument at position ${index + 1}`, 2);
63
+ }
64
+ const value = argv[index + 1];
65
+ if (!value || value.startsWith('--')) {
66
+ throw new HarnessError('local_validation', 'MISSING_ARGUMENT_VALUE', `${arg} requires a value`, 2);
67
+ }
68
+ values[arg] = value;
69
+ index += 1;
70
+ }
71
+ return { values, flags };
72
+ }
73
+
74
+ function envOrArg(parsed, argName, envName, env) {
75
+ return parsed.values[argName] ?? env[envName];
76
+ }
77
+
78
+ function positiveInt(raw, fallback, name) {
79
+ if (raw === undefined) return fallback;
80
+ const value = Number(raw);
81
+ if (!Number.isSafeInteger(value) || value <= 0) {
82
+ throw new HarnessError('local_validation', 'INVALID_NUMBER', `${name} must be a positive integer`, 2);
83
+ }
84
+ return value;
85
+ }
86
+
87
+ function isLocalWriteTarget(hostname) {
88
+ return ['localhost', '127.0.0.1', '::1', 'host.docker.internal'].includes(hostname);
89
+ }
90
+
91
+ export function loadHarnessConfig(argv, env = process.env) {
92
+ const parsed = parseHarnessArgs(argv);
93
+ if (parsed.flags.has('--help')) return { help: true };
94
+
95
+ const apiKey = envOrArg(parsed, '__never__', 'PRISMER_API_KEY', env);
96
+ const role = envOrArg(parsed, '--role', 'PRISMER_ROLE_SLUG', env)?.trim();
97
+ const workspaceId = envOrArg(parsed, '--workspace', 'PRISMER_WORKSPACE_ID', env)?.trim();
98
+ const task = envOrArg(parsed, '--task', 'PRISMER_TASK', env)?.trim();
99
+ const preflightOnly = parsed.flags.has('--preflight-only');
100
+ const missing = [
101
+ !apiKey && 'PRISMER_API_KEY',
102
+ !role && 'PRISMER_ROLE_SLUG/--role',
103
+ !workspaceId && 'PRISMER_WORKSPACE_ID/--workspace',
104
+ !task && !preflightOnly && 'PRISMER_TASK/--task',
105
+ ].filter(Boolean);
106
+ if (missing.length > 0) {
107
+ throw new HarnessError(
108
+ 'local_validation',
109
+ 'CONFIG_MISSING',
110
+ `missing required environment/config: ${missing.join(', ')}`,
111
+ 2,
112
+ );
113
+ }
114
+
115
+ let baseUrl;
116
+ try {
117
+ baseUrl = new URL((env.PRISMER_CLOUD_BASE || 'http://127.0.0.1:3000').replace(/\/$/, ''));
118
+ } catch {
119
+ throw new HarnessError('local_validation', 'INVALID_BASE_URL', 'PRISMER_CLOUD_BASE must be a valid URL', 2);
120
+ }
121
+ if (
122
+ !['http:', 'https:'].includes(baseUrl.protocol) ||
123
+ baseUrl.username ||
124
+ baseUrl.password ||
125
+ baseUrl.search ||
126
+ baseUrl.hash
127
+ ) {
128
+ throw new HarnessError(
129
+ 'local_validation',
130
+ 'UNSAFE_BASE_URL',
131
+ 'PRISMER_CLOUD_BASE must be a plain http(s) origin without credentials, query, or hash',
132
+ 2,
133
+ );
134
+ }
135
+ // Remote mutation authority is deliberately environment-owned. A prompt or
136
+ // copied command must not be able to grant itself authority with a CLI flag.
137
+ const allowRemote = ['1', 'true', 'yes'].includes(String(env.PRISMER_ALLOW_REMOTE_WRITE).toLowerCase());
138
+ if (!preflightOnly && !isLocalWriteTarget(baseUrl.hostname) && !allowRemote) {
139
+ throw new HarnessError(
140
+ 'local_validation',
141
+ 'REMOTE_WRITE_NOT_CONFIRMED',
142
+ 'remote mutation is blocked; set PRISMER_ALLOW_REMOTE_WRITE=1 after confirming the target environment',
143
+ 2,
144
+ );
145
+ }
146
+
147
+ const adapterName = envOrArg(parsed, '--adapter', 'PRISMER_AGENT_ADAPTER', env)?.trim() || 'hermes';
148
+ if (!['hermes', 'pi-core', 'claude-code'].includes(adapterName)) {
149
+ throw new HarnessError('local_validation', 'INVALID_ADAPTER', 'adapter must be hermes, pi-core, or claude-code', 2);
150
+ }
151
+ const requestId =
152
+ envOrArg(parsed, '--request-id', 'PRISMER_REQUEST_ID', env)?.trim() ||
153
+ `role-run-${randomUUID()}`;
154
+ if (requestId.length > 191) {
155
+ throw new HarnessError('local_validation', 'INVALID_REQUEST_ID', 'request id exceeds 191 characters', 2);
156
+ }
157
+
158
+ const config = {
159
+ apiKey,
160
+ baseUrl: baseUrl.toString().replace(/\/$/, ''),
161
+ role,
162
+ workspaceId,
163
+ task: task || '',
164
+ title:
165
+ envOrArg(parsed, '--title', 'PRISMER_TASK_TITLE', env)?.trim() || (task || `Preflight ${role}`).slice(0, 100),
166
+ handle: envOrArg(parsed, '--handle', 'PRISMER_AGENT_HANDLE', env)?.trim(),
167
+ displayName: envOrArg(parsed, '--display-name', 'PRISMER_AGENT_DISPLAY_NAME', env)?.trim(),
168
+ adapterName,
169
+ targetDaemonId: envOrArg(parsed, '--target-daemon', 'PRISMER_TARGET_DAEMON_ID', env)?.trim(),
170
+ requestId,
171
+ timeoutMs: positiveInt(envOrArg(parsed, '--timeout-ms', 'PRISMER_WORKFLOW_TIMEOUT_MS', env), 600_000, 'timeout'),
172
+ pollMs: positiveInt(envOrArg(parsed, '--poll-ms', 'PRISMER_WORKFLOW_POLL_MS', env), 2_000, 'poll interval'),
173
+ ledgerPath: resolve(
174
+ envOrArg(parsed, '--ledger', 'PRISMER_OPERATION_LEDGER', env) || `.prismer/operations/${requestId}.json`,
175
+ ),
176
+ wait: !parsed.flags.has('--no-wait'),
177
+ preflightOnly,
178
+ json: parsed.flags.has('--json'),
179
+ };
180
+ if (config.handle && !/^[a-z0-9][a-z0-9_-]{1,63}$/.test(config.handle)) {
181
+ throw new HarnessError('local_validation', 'INVALID_HANDLE', 'agent handle has an invalid shape', 2);
182
+ }
183
+ return config;
184
+ }
185
+
186
+ export function writeLedger(path, value) {
187
+ mkdirSync(dirname(path), { recursive: true });
188
+ const temporary = `${path}.tmp`;
189
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
190
+ renameSync(temporary, path);
191
+ }
192
+
193
+ export function readLedger(path) {
194
+ if (!existsSync(path)) return null;
195
+ try {
196
+ const value = JSON.parse(readFileSync(path, 'utf8'));
197
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
198
+ } catch {
199
+ throw new HarnessError(
200
+ 'local_validation',
201
+ 'INVALID_OPERATION_LEDGER',
202
+ 'the operation ledger exists but is not valid JSON',
203
+ 2,
204
+ );
205
+ }
206
+ }
207
+
208
+ export function invocationHash(config) {
209
+ return createHash('sha256')
210
+ .update(
211
+ JSON.stringify({
212
+ requestId: config.requestId,
213
+ role: config.role,
214
+ workspaceId: config.workspaceId,
215
+ task: config.task,
216
+ title: config.title,
217
+ handle: config.handle ?? null,
218
+ displayName: config.displayName ?? null,
219
+ adapterName: config.adapterName,
220
+ targetDaemonId: config.targetDaemonId ?? null,
221
+ }),
222
+ )
223
+ .digest('hex');
224
+ }
225
+
226
+ export function taskIdempotencyKey(requestId) {
227
+ return `role-task-${createHash('sha256').update(requestId).digest('hex')}`;
228
+ }
229
+
230
+ export async function requestJson(config, method, path, body, stage) {
231
+ const controller = new AbortController();
232
+ const timer = setTimeout(() => controller.abort(), Math.min(config.timeoutMs, 120_000));
233
+ let response;
234
+ try {
235
+ response = await fetch(`${config.baseUrl}${path}`, {
236
+ method,
237
+ headers: {
238
+ authorization: `Bearer ${config.apiKey}`,
239
+ 'content-type': 'application/json',
240
+ ...(body?.idempotencyKey ? { 'x-idempotency-key': body.idempotencyKey } : {}),
241
+ },
242
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
243
+ signal: controller.signal,
244
+ });
245
+ } catch (error) {
246
+ const message = error?.name === 'AbortError' ? 'request timed out' : error?.message || String(error);
247
+ throw new HarnessError(stage, 'NETWORK_ERROR', message, 4);
248
+ } finally {
249
+ clearTimeout(timer);
250
+ }
251
+ const payload = await response.json().catch(() => null);
252
+ if (!response.ok || payload?.ok === false) {
253
+ const serverError = payload?.error;
254
+ throw new HarnessError(
255
+ stage,
256
+ serverError?.code || `HTTP_${response.status}`,
257
+ serverError?.message || serverError || `request failed with HTTP ${response.status}`,
258
+ response.status === 401 || response.status === 403 ? 3 : response.status >= 500 ? 4 : 5,
259
+ { status: response.status, retryable: serverError?.retryable === true },
260
+ );
261
+ }
262
+ return { status: response.status, data: payload?.data ?? payload };
263
+ }
264
+
265
+ export function sleep(ms) {
266
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
267
+ }