@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.
- package/CHANGELOG.md +3430 -0
- package/README.md +34 -12
- package/apc/skills/FIELD-DICTIONARY.md +111 -0
- package/apc/skills/bug-reproduce/SKILL.md +150 -0
- package/apc/skills/bug-reproduce/skill.json +96 -0
- package/apc/skills/code-review/SKILL.md +198 -0
- package/apc/skills/code-review/skill.json +124 -0
- package/apc/skills/design-review/SKILL.md +122 -0
- package/apc/skills/design-review/skill.json +88 -0
- package/apc/skills/doc-sync/SKILL.md +168 -0
- package/apc/skills/doc-sync/skill.json +81 -0
- package/apc/skills/env-doctor/SKILL.md +194 -0
- package/apc/skills/env-doctor/skill.json +209 -0
- package/apc/skills/git-ops/SKILL.md +189 -0
- package/apc/skills/git-ops/skill.json +94 -0
- package/apc/skills/impact-trace/SKILL.md +168 -0
- package/apc/skills/impact-trace/skill.json +104 -0
- package/apc/skills/observability/SKILL.md +195 -0
- package/apc/skills/observability/skill.json +116 -0
- package/apc/skills/release-db-config-sync/SKILL.md +186 -0
- package/apc/skills/release-db-config-sync/skill.json +109 -0
- package/apc/skills/release-ota-promote/SKILL.md +195 -0
- package/apc/skills/release-ota-promote/skill.json +176 -0
- package/apc/skills/release-preflight/SKILL.md +174 -0
- package/apc/skills/release-preflight/skill.json +175 -0
- package/apc/skills/release-rollback/SKILL.md +214 -0
- package/apc/skills/release-rollback/skill.json +230 -0
- package/apc/skills/release-tag/SKILL.md +194 -0
- package/apc/skills/release-tag/skill.json +94 -0
- package/apc/skills/releasing-prod/SKILL.md +49 -0
- package/apc/skills/releasing-test/SKILL.md +135 -0
- package/apc/skills/sdk-release/SKILL.md +200 -0
- package/apc/skills/spec-intake/SKILL.md +169 -0
- package/apc/skills/spec-intake/skill.json +93 -0
- package/apc/skills/test-result-feedback/SKILL.md +239 -0
- package/apc/skills/test-result-feedback/skill.json +193 -0
- package/apc/skills/test-runner/SKILL.md +169 -0
- package/apc/skills/test-runner/skill.json +103 -0
- package/apc/skills/ui-align/SKILL.md +209 -0
- package/apc/skills/ui-align/skill.json +114 -0
- package/apc/skills/ui-canvas/SKILL.md +148 -0
- package/apc/skills/ui-canvas/skill.json +127 -0
- package/built-in-skills/agent-coordination/SKILL.md +59 -37
- package/built-in-skills/agent-meta/SKILL.md +1 -0
- package/built-in-skills/assets/SKILL.md +8 -6
- package/built-in-skills/browser-use/SKILL.md +93 -0
- package/built-in-skills/canvas-design/SKILL.md +1 -0
- package/built-in-skills/claim-agent-ownership/SKILL.md +3 -2
- package/built-in-skills/claude-api/SKILL.md +1 -0
- package/built-in-skills/codebase-design/DEEPENING.md +37 -0
- package/built-in-skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/built-in-skills/codebase-design/LICENSE +21 -0
- package/built-in-skills/codebase-design/SKILL.md +116 -0
- package/built-in-skills/conversation-compaction/SKILL.md +114 -0
- package/built-in-skills/council-creator/SKILL.md +426 -0
- package/built-in-skills/diagnosing-bugs/LICENSE +21 -0
- package/built-in-skills/diagnosing-bugs/SKILL.md +136 -0
- package/built-in-skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/built-in-skills/doc-coauthoring/SKILL.md +1 -0
- package/built-in-skills/document-generation/SKILL.md +105 -0
- package/built-in-skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/built-in-skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/built-in-skills/domain-modeling/LICENSE +21 -0
- package/built-in-skills/domain-modeling/SKILL.md +76 -0
- package/built-in-skills/frontend-design/SKILL.md +1 -0
- package/built-in-skills/human-approval/SKILL.md +17 -2
- package/built-in-skills/image-generate/SKILL.md +103 -302
- package/built-in-skills/image-generate/scripts/generate-and-deliver.mjs +289 -0
- package/built-in-skills/ingest/SKILL.md +13 -45
- package/built-in-skills/internal-comms/SKILL.md +1 -0
- package/built-in-skills/liteparse/SKILL.md +130 -110
- package/built-in-skills/mcp-builder/SKILL.md +1 -0
- package/built-in-skills/memory/SKILL.md +420 -55
- package/built-in-skills/memory-dream/SKILL.md +339 -0
- package/built-in-skills/office-artifacts/SKILL.md +17 -4
- package/built-in-skills/okr/SKILL.md +154 -0
- package/built-in-skills/persona/SKILL.md +81 -0
- package/built-in-skills/persona-generator/SKILL.md +296 -0
- package/built-in-skills/pkf-svg/SKILL.md +253 -0
- package/built-in-skills/pkf-writing/SKILL.md +236 -0
- package/built-in-skills/prismer-im-collab/SKILL.md +26 -6
- package/built-in-skills/proactivity/SKILL.md +84 -0
- package/built-in-skills/remotion/SKILL.md +431 -0
- package/built-in-skills/role-builder/SKILL.md +203 -0
- package/built-in-skills/role-builder/scripts/author-role.mjs +334 -0
- package/built-in-skills/role-builder/scripts/ingest-role.mjs +223 -0
- package/built-in-skills/role-builder/scripts/instantiate-and-run.mjs +290 -0
- package/built-in-skills/role-builder/scripts/operation-harness.mjs +267 -0
- package/built-in-skills/skill-authoring/SKILL.md +110 -100
- package/built-in-skills/skill-authoring/skill.json +3 -3
- package/built-in-skills/skill-builder/SKILL.md +171 -0
- package/built-in-skills/skill-builder/scripts/ingest.mjs +265 -0
- package/built-in-skills/skill-creator/SKILL.md +165 -423
- package/built-in-skills/skill-creator/references/external-library-import.md +110 -0
- package/built-in-skills/skill-creator/scripts/import-library.mjs +475 -0
- package/built-in-skills/slack-gif-creator/SKILL.md +20 -0
- package/built-in-skills/tasks/SKILL.md +38 -23
- package/built-in-skills/tdd/LICENSE +21 -0
- package/built-in-skills/tdd/SKILL.md +110 -0
- package/built-in-skills/tdd/mocking.md +59 -0
- package/built-in-skills/tdd/refactoring.md +10 -0
- package/built-in-skills/tdd/tests.md +61 -0
- package/built-in-skills/team/SKILL.md +2 -1
- package/built-in-skills/web-artifacts-builder/SKILL.md +1 -0
- package/built-in-skills/webapp-testing/SKILL.md +1 -0
- package/built-in-skills/wechat-pay/SKILL.md +59 -0
- package/dist/cli.cjs +71872 -19960
- package/dist/cli.js +71803 -19846
- package/dist/index.cjs +72010 -19966
- package/dist/index.d.cts +4258 -712
- package/dist/index.d.ts +4258 -712
- package/dist/index.js +72156 -20118
- package/package.json +37 -6
- package/plugins/memory/prismer/__init__.py +1211 -0
- package/plugins/memory/prismer/plugin.yaml +8 -0
- package/plugins/memory/prismer/tool-schemas.generated.json +249 -0
- package/plugins/tools/prismer-recall/__init__.py +282 -0
- package/plugins/tools/prismer-recall/plugin.yaml +15 -0
- package/built-in-skills/memory-curation/SKILL.md +0 -135
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
export class RoleAuthoringError extends Error {
|
|
9
|
+
constructor(stage, code, message, exitCode = 1, details = undefined) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'RoleAuthoringError';
|
|
12
|
+
this.stage = stage;
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.exitCode = exitCode;
|
|
15
|
+
this.details = details;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SECRET_OPTIONS = new Set(['--api-key', '--token', '--jwt', '--secret', '--password']);
|
|
20
|
+
const BOOLEAN_OPTIONS = new Set(['--json', '--preflight-only', '--help']);
|
|
21
|
+
|
|
22
|
+
function optionName(arg) {
|
|
23
|
+
const equalsAt = typeof arg === 'string' ? arg.indexOf('=') : -1;
|
|
24
|
+
return equalsAt === -1 ? arg : arg.slice(0, equalsAt);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function enabled(value) {
|
|
28
|
+
return ['1', 'true', 'yes'].includes(String(value ?? '').toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function localTarget(hostname) {
|
|
32
|
+
return ['localhost', '127.0.0.1', '::1', 'host.docker.internal'].includes(hostname);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function roleRevision(rolePath) {
|
|
36
|
+
const stat = statSync(rolePath);
|
|
37
|
+
const hash = createHash('sha256');
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
for (const name of ['role.json', 'SOUL.md']) {
|
|
40
|
+
const file = resolve(rolePath, name);
|
|
41
|
+
if (!existsSync(file)) continue;
|
|
42
|
+
hash.update(name);
|
|
43
|
+
hash.update('\0');
|
|
44
|
+
hash.update(readFileSync(file));
|
|
45
|
+
hash.update('\0');
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
hash.update(readFileSync(rolePath));
|
|
49
|
+
}
|
|
50
|
+
return hash.digest('hex');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function loadRoleAuthoringConfig(argv, env = process.env) {
|
|
54
|
+
const flags = new Set();
|
|
55
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
56
|
+
const arg = argv[index];
|
|
57
|
+
const name = optionName(arg);
|
|
58
|
+
if (SECRET_OPTIONS.has(name)) {
|
|
59
|
+
throw new RoleAuthoringError(
|
|
60
|
+
'local_validation',
|
|
61
|
+
'SECRET_ARGUMENT_FORBIDDEN',
|
|
62
|
+
`${name} is forbidden; credentials must come from the injected environment`,
|
|
63
|
+
2,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!BOOLEAN_OPTIONS.has(arg)) {
|
|
67
|
+
throw new RoleAuthoringError(
|
|
68
|
+
'local_validation',
|
|
69
|
+
'UNKNOWN_ARGUMENT',
|
|
70
|
+
`unknown argument at position ${index + 1}`,
|
|
71
|
+
2,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
flags.add(arg);
|
|
75
|
+
}
|
|
76
|
+
if (flags.has('--help')) return { help: true };
|
|
77
|
+
|
|
78
|
+
const rawRolePath = env.PRISMER_ROLE_BUNDLE?.trim();
|
|
79
|
+
if (!rawRolePath) {
|
|
80
|
+
throw new RoleAuthoringError('local_validation', 'CONFIG_MISSING', 'PRISMER_ROLE_BUNDLE is required', 2);
|
|
81
|
+
}
|
|
82
|
+
const rolePath = resolve(rawRolePath);
|
|
83
|
+
if (!existsSync(rolePath)) {
|
|
84
|
+
throw new RoleAuthoringError('local_validation', 'ROLE_BUNDLE_NOT_FOUND', 'PRISMER_ROLE_BUNDLE does not exist', 2);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let baseUrl;
|
|
88
|
+
try {
|
|
89
|
+
baseUrl = new URL((env.PRISMER_CLOUD_BASE || 'http://127.0.0.1:3000').replace(/\/$/, ''));
|
|
90
|
+
} catch {
|
|
91
|
+
throw new RoleAuthoringError('local_validation', 'INVALID_BASE_URL', 'PRISMER_CLOUD_BASE must be a valid URL', 2);
|
|
92
|
+
}
|
|
93
|
+
if (
|
|
94
|
+
!['http:', 'https:'].includes(baseUrl.protocol) ||
|
|
95
|
+
baseUrl.username ||
|
|
96
|
+
baseUrl.password ||
|
|
97
|
+
baseUrl.search ||
|
|
98
|
+
baseUrl.hash
|
|
99
|
+
) {
|
|
100
|
+
throw new RoleAuthoringError(
|
|
101
|
+
'local_validation',
|
|
102
|
+
'UNSAFE_BASE_URL',
|
|
103
|
+
'PRISMER_CLOUD_BASE must be a plain http(s) origin without credentials, query, or hash',
|
|
104
|
+
2,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const preflightOnly = flags.has('--preflight-only');
|
|
108
|
+
if (!preflightOnly && !localTarget(baseUrl.hostname) && !enabled(env.PRISMER_ALLOW_REMOTE_WRITE)) {
|
|
109
|
+
throw new RoleAuthoringError(
|
|
110
|
+
'local_validation',
|
|
111
|
+
'REMOTE_WRITE_NOT_CONFIRMED',
|
|
112
|
+
'remote mutation is blocked; set PRISMER_ALLOW_REMOTE_WRITE=1 after confirming the target environment',
|
|
113
|
+
2,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const revision = roleRevision(rolePath);
|
|
118
|
+
return {
|
|
119
|
+
rolePath,
|
|
120
|
+
revision,
|
|
121
|
+
cloudBin: env.PRISMER_CLOUD_BIN?.trim() || 'cloud',
|
|
122
|
+
baseUrl: baseUrl.toString().replace(/\/$/, ''),
|
|
123
|
+
ledgerPath: resolve(
|
|
124
|
+
env.PRISMER_ROLE_AUTHORING_LEDGER?.trim() ||
|
|
125
|
+
`.prismer/operations/role-author-${createHash('sha256').update(rolePath).digest('hex').slice(0, 16)}.json`,
|
|
126
|
+
),
|
|
127
|
+
publish: enabled(env.PRISMER_ROLE_PUBLISH),
|
|
128
|
+
preflightOnly,
|
|
129
|
+
json: flags.has('--json'),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function defaultRunCloud(config, args) {
|
|
134
|
+
const result = spawnSync(config.cloudBin, args, {
|
|
135
|
+
encoding: 'utf8',
|
|
136
|
+
env: { ...process.env, PRISMER_CLOUD_BASE: config.baseUrl },
|
|
137
|
+
shell: false,
|
|
138
|
+
});
|
|
139
|
+
if (result.error) {
|
|
140
|
+
throw new RoleAuthoringError(
|
|
141
|
+
'local_validation',
|
|
142
|
+
'CLOUD_CLI_UNAVAILABLE',
|
|
143
|
+
`could not execute the configured Cloud CLI: ${result.error.message}`,
|
|
144
|
+
2,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
ok: result.status === 0,
|
|
149
|
+
status: result.status,
|
|
150
|
+
stdout: result.stdout || '',
|
|
151
|
+
stderr: result.stderr || '',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseJson(result, stage) {
|
|
156
|
+
const raw = String(result.stdout || '').trim();
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(raw);
|
|
159
|
+
} catch {
|
|
160
|
+
const start = raw.indexOf('{');
|
|
161
|
+
const end = raw.lastIndexOf('}');
|
|
162
|
+
if (start >= 0 && end > start) {
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(raw.slice(start, end + 1));
|
|
165
|
+
} catch {
|
|
166
|
+
// Fall through.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw new RoleAuthoringError(stage, 'INVALID_CLI_JSON', 'cloud CLI did not return valid JSON', 5);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function cliError(result) {
|
|
174
|
+
return (
|
|
175
|
+
String(result.stderr || '')
|
|
176
|
+
.trim()
|
|
177
|
+
.slice(0, 2_000) || `cloud CLI exited ${String(result.status)}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function writeLedger(path, ledger) {
|
|
182
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
183
|
+
const temporary = `${path}.tmp`;
|
|
184
|
+
writeFileSync(temporary, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
|
|
185
|
+
renameSync(temporary, path);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function readLedger(path) {
|
|
189
|
+
if (!existsSync(path)) return null;
|
|
190
|
+
try {
|
|
191
|
+
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
192
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
193
|
+
} catch {
|
|
194
|
+
throw new RoleAuthoringError(
|
|
195
|
+
'local_validation',
|
|
196
|
+
'INVALID_AUTHORING_LEDGER',
|
|
197
|
+
'the role authoring ledger exists but is not valid JSON',
|
|
198
|
+
2,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function runRoleAuthoring(config, runCloud = defaultRunCloud) {
|
|
204
|
+
const previous = readLedger(config.ledgerPath);
|
|
205
|
+
if (
|
|
206
|
+
previous &&
|
|
207
|
+
(previous.rolePath !== config.rolePath ||
|
|
208
|
+
previous.revision !== config.revision ||
|
|
209
|
+
Boolean(previous.publish) !== config.publish)
|
|
210
|
+
) {
|
|
211
|
+
throw new RoleAuthoringError(
|
|
212
|
+
'local_validation',
|
|
213
|
+
'AUTHORING_LEDGER_CONFLICT',
|
|
214
|
+
'the role authoring ledger belongs to different bundle content or destination',
|
|
215
|
+
2,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (previous?.stage === 'completed') return previous;
|
|
219
|
+
if (previous?.stage === 'created' && previous.slug) {
|
|
220
|
+
// The server push completed and its canonical slug was durably recorded;
|
|
221
|
+
// resume from readback instead of pushing again (which intentionally bumps
|
|
222
|
+
// a role version even when its content is identical).
|
|
223
|
+
const shown = await runCloud(config, ['role', 'show', previous.slug, '--json']);
|
|
224
|
+
if (!shown.ok) {
|
|
225
|
+
throw new RoleAuthoringError(
|
|
226
|
+
'readback',
|
|
227
|
+
'ROLE_READBACK_FAILED',
|
|
228
|
+
'a previously-created role could not be read back; refusing an automatic second push',
|
|
229
|
+
5,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
previous.readback = parseJson(shown, 'readback');
|
|
233
|
+
previous.stage = 'completed';
|
|
234
|
+
previous.completedAt = new Date().toISOString();
|
|
235
|
+
previous.updatedAt = previous.completedAt;
|
|
236
|
+
writeLedger(config.ledgerPath, previous);
|
|
237
|
+
return previous;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const validated = await runCloud(config, ['role', 'validate', config.rolePath, '--json']);
|
|
241
|
+
if (!validated.ok) {
|
|
242
|
+
throw new RoleAuthoringError('validation', 'ROLE_VALIDATION_FAILED', cliError(validated), 2);
|
|
243
|
+
}
|
|
244
|
+
const validation = parseJson(validated, 'validation');
|
|
245
|
+
if (validation.ok === false) {
|
|
246
|
+
throw new RoleAuthoringError('validation', 'ROLE_VALIDATION_FAILED', 'role bundle failed local validation', 2);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const tested = await runCloud(config, ['role', 'test', config.rolePath, '--json']);
|
|
250
|
+
if (!tested.ok) {
|
|
251
|
+
throw new RoleAuthoringError('preflight', 'ROLE_PREFLIGHT_FAILED', cliError(tested), 5);
|
|
252
|
+
}
|
|
253
|
+
const test = parseJson(tested, 'preflight');
|
|
254
|
+
if (test.ok === false) {
|
|
255
|
+
throw new RoleAuthoringError('preflight', 'ROLE_PREFLIGHT_FAILED', 'required Skill resolution failed', 5);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const ledger = {
|
|
259
|
+
schemaVersion: 1,
|
|
260
|
+
rolePath: config.rolePath,
|
|
261
|
+
revision: config.revision,
|
|
262
|
+
publish: config.publish,
|
|
263
|
+
slug: validation.slug ?? test.slug ?? null,
|
|
264
|
+
stage: 'preflight_complete',
|
|
265
|
+
validation,
|
|
266
|
+
test,
|
|
267
|
+
updatedAt: new Date().toISOString(),
|
|
268
|
+
};
|
|
269
|
+
writeLedger(config.ledgerPath, ledger);
|
|
270
|
+
if (config.preflightOnly) return ledger;
|
|
271
|
+
|
|
272
|
+
const createArgs = ['role', 'create', config.rolePath, '--mine', '--json'];
|
|
273
|
+
if (config.publish) createArgs.push('--publish');
|
|
274
|
+
const created = await runCloud(config, createArgs);
|
|
275
|
+
if (!created.ok) {
|
|
276
|
+
throw new RoleAuthoringError('create', 'ROLE_CREATE_FAILED', cliError(created), 5);
|
|
277
|
+
}
|
|
278
|
+
const creation = parseJson(created, 'create');
|
|
279
|
+
if (!creation.slug) {
|
|
280
|
+
throw new RoleAuthoringError('create', 'CANONICAL_SLUG_MISSING', 'create response omitted the role slug', 5);
|
|
281
|
+
}
|
|
282
|
+
ledger.creation = creation;
|
|
283
|
+
ledger.slug = creation.slug;
|
|
284
|
+
ledger.stage = 'created';
|
|
285
|
+
ledger.updatedAt = new Date().toISOString();
|
|
286
|
+
writeLedger(config.ledgerPath, ledger);
|
|
287
|
+
|
|
288
|
+
const shown = await runCloud(config, ['role', 'show', creation.slug, '--json']);
|
|
289
|
+
if (!shown.ok) {
|
|
290
|
+
throw new RoleAuthoringError('readback', 'ROLE_READBACK_FAILED', cliError(shown), 5);
|
|
291
|
+
}
|
|
292
|
+
ledger.readback = parseJson(shown, 'readback');
|
|
293
|
+
ledger.stage = 'completed';
|
|
294
|
+
ledger.completedAt = new Date().toISOString();
|
|
295
|
+
ledger.updatedAt = ledger.completedAt;
|
|
296
|
+
writeLedger(config.ledgerPath, ledger);
|
|
297
|
+
return ledger;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const HELP = `Usage:
|
|
301
|
+
# PRISMER_ROLE_BUNDLE and cloud authentication are injected in env
|
|
302
|
+
node scripts/author-role.mjs [--preflight-only] [--json]
|
|
303
|
+
|
|
304
|
+
Optional env: PRISMER_ROLE_PUBLISH=1, PRISMER_ROLE_AUTHORING_LEDGER,
|
|
305
|
+
PRISMER_CLOUD_BIN, PRISMER_CLOUD_BASE, PRISMER_ALLOW_REMOTE_WRITE=1.`;
|
|
306
|
+
|
|
307
|
+
async function main() {
|
|
308
|
+
let config;
|
|
309
|
+
try {
|
|
310
|
+
config = loadRoleAuthoringConfig(process.argv.slice(2));
|
|
311
|
+
if (config.help) {
|
|
312
|
+
process.stdout.write(`${HELP}\n`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const result = await runRoleAuthoring(config);
|
|
316
|
+
if (config.json) process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
317
|
+
else {
|
|
318
|
+
process.stdout.write(
|
|
319
|
+
`[role-builder] ${result.stage}: role=${result.slug || 'unresolved'} ledger=${config.ledgerPath}\n`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
const failure =
|
|
324
|
+
error instanceof RoleAuthoringError
|
|
325
|
+
? error
|
|
326
|
+
: new RoleAuthoringError('internal', 'UNEXPECTED_ERROR', error?.message || String(error), 1);
|
|
327
|
+
process.stderr.write(
|
|
328
|
+
`${JSON.stringify({ ok: false, stage: failure.stage, error: { code: failure.code, message: failure.message, details: failure.details }, ...(config?.ledgerPath ? { ledger: config.ledgerPath } : {}) })}\n`,
|
|
329
|
+
);
|
|
330
|
+
process.exitCode = failure.exitCode;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (import.meta.url === `file://${process.argv[1]}`) await main();
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// role-builder ingest — material→role template→live, optionally applied, via API key.
|
|
3
|
+
//
|
|
4
|
+
// Zero runtime dependencies (Node ≥18 built-ins). Reads a SS-02 role.json,
|
|
5
|
+
// POSTs it to the real role-template endpoint with an `sk-prismer-*` admin API
|
|
6
|
+
// key, and optionally applies it to a live agent.
|
|
7
|
+
//
|
|
8
|
+
// Accepts EITHER a single role.json OR a role bundle directory (role.json +
|
|
9
|
+
// optional SOUL.md). When a dir + SOUL.md is given, SOUL.md's markdown becomes
|
|
10
|
+
// operatingPrinciples (Hermes-compatible persona, doc 16 §5.4). Mirrors the
|
|
11
|
+
// `cloud role create` CLI logic so the offline path matches.
|
|
12
|
+
//
|
|
13
|
+
// Usage:
|
|
14
|
+
// PRISMER_API_KEY=sk-prismer-... [PRISMER_CLOUD_BASE=http://127.0.0.1:3000] \
|
|
15
|
+
// node scripts/ingest-role.mjs <role.json|role-dir> [--publish|--admin-catalog] [--apply --agent <imUserId> --workspace-id <wsId>] [--json]
|
|
16
|
+
//
|
|
17
|
+
// Notes (product204/16 §2.3 — CLI 三入口默认私域):
|
|
18
|
+
// • DEFAULT (no flag) is the caller's PRIVATE Studio `/mine` (ownerAgentId=
|
|
19
|
+
// caller, visibility=private) via POST /api/im/role-templates/mine —
|
|
20
|
+
// editable, then publishable. Any authenticated key works (no admin
|
|
21
|
+
// needed). `--mine` is accepted as an explicit no-op for back-compat.
|
|
22
|
+
// • `--publish` (alias `--admin-catalog`) creates on the admin-only public
|
|
23
|
+
// catalog path (POST /api/im/role-templates, SS-02 §4.1) — the key must
|
|
24
|
+
// map to an admin email, else the server returns 403. Publication is an
|
|
25
|
+
// EXPLICIT act, never the default (16 §2.3).
|
|
26
|
+
// • APPLY is admin OR the target agent's workspace owner.
|
|
27
|
+
|
|
28
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
|
|
31
|
+
function die(msg, code = 1) {
|
|
32
|
+
console.error(`[role-builder] ✗ ${msg}`);
|
|
33
|
+
process.exit(code);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
const secretOptions = new Set(['--api-key', '--token', '--jwt', '--secret', '--password']);
|
|
38
|
+
let rolePath;
|
|
39
|
+
let wantApply = false;
|
|
40
|
+
let wantPublish = false;
|
|
41
|
+
let asJson = false;
|
|
42
|
+
let agentId;
|
|
43
|
+
let workspaceId;
|
|
44
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
45
|
+
const arg = args[index];
|
|
46
|
+
const equalsAt = arg.indexOf('=');
|
|
47
|
+
const option = equalsAt === -1 ? arg : arg.slice(0, equalsAt);
|
|
48
|
+
if (secretOptions.has(option)) {
|
|
49
|
+
die(`${option} is forbidden; credentials must come from environment variables`, 2);
|
|
50
|
+
}
|
|
51
|
+
if (arg === '--apply') {
|
|
52
|
+
wantApply = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (arg === '--publish' || arg === '--admin-catalog') {
|
|
56
|
+
wantPublish = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (arg === '--mine') continue;
|
|
60
|
+
if (arg === '--json') {
|
|
61
|
+
asJson = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (arg === '--agent' || arg === '--workspace-id') {
|
|
65
|
+
const value = args[index + 1];
|
|
66
|
+
if (!value || value.startsWith('--')) die(`${arg} requires a value`, 2);
|
|
67
|
+
if (arg === '--agent') agentId = value;
|
|
68
|
+
else workspaceId = value;
|
|
69
|
+
index += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg.startsWith('--')) die(`unknown argument at position ${index + 1}`, 2);
|
|
73
|
+
if (rolePath) die('only one role path may be supplied', 2);
|
|
74
|
+
rolePath = arg;
|
|
75
|
+
}
|
|
76
|
+
// product204/16 §2.3 — private `/mine` is the DEFAULT; the public catalog is an
|
|
77
|
+
// explicit `--publish` (alias `--admin-catalog`). `--mine` stays accepted as a
|
|
78
|
+
// no-op for back-compat with pre-204 invocations.
|
|
79
|
+
const wantMine = !wantPublish;
|
|
80
|
+
|
|
81
|
+
const API_KEY = process.env.PRISMER_API_KEY;
|
|
82
|
+
const BASE = (process.env.PRISMER_CLOUD_BASE || 'http://127.0.0.1:3000').replace(/\/$/, '');
|
|
83
|
+
|
|
84
|
+
if (!rolePath) die('usage: node ingest-role.mjs <role.json|role-dir> [--publish|--admin-catalog] [--apply --agent <id> --workspace-id <id>] [--json]');
|
|
85
|
+
if (!API_KEY) die('PRISMER_API_KEY env is required (sk-prismer-*; --mine needs any key, the admin catalog path needs an admin email)');
|
|
86
|
+
if (wantApply && !agentId) die('--apply requires --agent <imUserId>');
|
|
87
|
+
let target;
|
|
88
|
+
try {
|
|
89
|
+
target = new URL(BASE);
|
|
90
|
+
} catch {
|
|
91
|
+
die('PRISMER_CLOUD_BASE must be a valid URL', 2);
|
|
92
|
+
}
|
|
93
|
+
if (
|
|
94
|
+
!['http:', 'https:'].includes(target.protocol) ||
|
|
95
|
+
target.username ||
|
|
96
|
+
target.password ||
|
|
97
|
+
target.search ||
|
|
98
|
+
target.hash
|
|
99
|
+
) {
|
|
100
|
+
die('PRISMER_CLOUD_BASE must be a plain http(s) origin without credentials, query, or hash', 2);
|
|
101
|
+
}
|
|
102
|
+
const isLocalTarget = ['localhost', '127.0.0.1', '::1', 'host.docker.internal'].includes(target.hostname);
|
|
103
|
+
const allowRemote = ['1', 'true', 'yes'].includes(String(process.env.PRISMER_ALLOW_REMOTE_WRITE).toLowerCase());
|
|
104
|
+
if (!isLocalTarget && !allowRemote) {
|
|
105
|
+
die('remote mutation is blocked; set PRISMER_ALLOW_REMOTE_WRITE=1 after confirming the target environment', 2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Read a role authoring unit — file OR directory (mirrors `cloud role create` /
|
|
109
|
+
// runtime bundle.readRoleBundle). A dir folds SOUL.md → operatingPrinciples.
|
|
110
|
+
function readRoleBundle(p) {
|
|
111
|
+
let st;
|
|
112
|
+
try {
|
|
113
|
+
st = statSync(p);
|
|
114
|
+
} catch {
|
|
115
|
+
die(`role path not found: ${p}`);
|
|
116
|
+
}
|
|
117
|
+
if (!st.isDirectory()) {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
120
|
+
} catch (err) {
|
|
121
|
+
die(`could not read/parse ${p}: ${err?.message || err}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Directory bundle: role.json (required) + SOUL.md (optional persona).
|
|
125
|
+
let r;
|
|
126
|
+
try {
|
|
127
|
+
r = JSON.parse(readFileSync(join(p, 'role.json'), 'utf8'));
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (err?.code === 'ENOENT') die(`role bundle dir is missing role.json: ${join(p, 'role.json')}`);
|
|
130
|
+
die(`could not read/parse ${join(p, 'role.json')}: ${err?.message || err}`);
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const soul = readFileSync(join(p, 'SOUL.md'), 'utf8');
|
|
134
|
+
// SOUL.md is the persona source-of-truth → operatingPrinciples (markdown).
|
|
135
|
+
if (soul.trim() !== '') r.operatingPrinciples = soul;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err?.code !== 'ENOENT') die(`could not read SOUL.md: ${err?.message || err}`);
|
|
138
|
+
// SOUL.md absent is allowed.
|
|
139
|
+
}
|
|
140
|
+
return r;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const role = readRoleBundle(rolePath);
|
|
144
|
+
|
|
145
|
+
const slug = role.slug || role.templateName;
|
|
146
|
+
if (!slug && !role.name && !role.displayName) die('role.json must have slug or name/displayName');
|
|
147
|
+
|
|
148
|
+
if (role.requiredSkills && !Array.isArray(role.requiredSkills)) {
|
|
149
|
+
die('requiredSkills must be an array of { skillSlug, required }');
|
|
150
|
+
}
|
|
151
|
+
for (const rs of role.requiredSkills || []) {
|
|
152
|
+
if (!rs || typeof rs.skillSlug !== 'string') {
|
|
153
|
+
die(`each requiredSkills entry needs a string skillSlug — got ${JSON.stringify(rs)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function api(method, path, body) {
|
|
158
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
159
|
+
method,
|
|
160
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${API_KEY}` },
|
|
161
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
162
|
+
});
|
|
163
|
+
let json = null;
|
|
164
|
+
try {
|
|
165
|
+
json = await res.json();
|
|
166
|
+
} catch {
|
|
167
|
+
/* non-JSON */
|
|
168
|
+
}
|
|
169
|
+
return { status: res.status, json };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
(async () => {
|
|
173
|
+
// product204/16 §2.3 — DEFAULT is the owner-scoped create path (private role
|
|
174
|
+
// owned by the caller, lands in Studio `/mine`); `--publish`/`--admin-catalog`
|
|
175
|
+
// is the EXPLICIT admin catalog path (publishes straight to the Marketplace).
|
|
176
|
+
const createPath = wantMine ? '/api/im/role-templates/mine' : '/api/im/role-templates';
|
|
177
|
+
const created = await api('POST', createPath, role);
|
|
178
|
+
if (created.status === 403) {
|
|
179
|
+
die(
|
|
180
|
+
wantMine
|
|
181
|
+
? 'create returned 403 — the API key does not resolve to a workspace user (auth failed).'
|
|
182
|
+
: 'create returned 403 — the API key does not map to an admin email (role create is admin-only). Use --mine for an owner-scoped private role.',
|
|
183
|
+
3,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (created.status === 409) die(`role slug already exists (409). Use update or a new slug.`, 2);
|
|
187
|
+
if (created.status !== 201 || !created.json?.ok) {
|
|
188
|
+
die(`create failed (HTTP ${created.status}): ${created.json?.error || 'unknown error'}`);
|
|
189
|
+
}
|
|
190
|
+
const tpl = created.json.data;
|
|
191
|
+
const result = {
|
|
192
|
+
slug: tpl.slug,
|
|
193
|
+
agentType: tpl.agentType,
|
|
194
|
+
taskAuthority: tpl.taskAuthority,
|
|
195
|
+
requiredSkills: Array.isArray(role.requiredSkills) ? role.requiredSkills.length : 0,
|
|
196
|
+
applied: false,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (wantApply) {
|
|
200
|
+
const apply = await api('POST', `/api/im/role-templates/${encodeURIComponent(tpl.slug)}/apply`, {
|
|
201
|
+
agentId,
|
|
202
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
203
|
+
});
|
|
204
|
+
if (apply.status >= 200 && apply.status < 300 && apply.json?.ok !== false) {
|
|
205
|
+
result.applied = agentId;
|
|
206
|
+
} else if (apply.status === 403) {
|
|
207
|
+
result.applyError = '403 — need admin or the target agent workspace owner';
|
|
208
|
+
} else {
|
|
209
|
+
result.applyError = `HTTP ${apply.status}: ${apply.json?.error || 'apply failed'}`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (asJson) {
|
|
214
|
+
console.log(JSON.stringify(result, null, 2));
|
|
215
|
+
} else {
|
|
216
|
+
console.log(`[role-builder] ✓ ingested role ${result.slug}`);
|
|
217
|
+
console.log(` type: ${result.agentType} / authority=${result.taskAuthority}`);
|
|
218
|
+
console.log(` skills: ${result.requiredSkills}`);
|
|
219
|
+
console.log(` applied: ${result.applied}${result.applyError ? ` (${result.applyError})` : ''}`);
|
|
220
|
+
console.log(` verify: GET /api/im/role-templates/${result.slug}`);
|
|
221
|
+
}
|
|
222
|
+
if (result.applyError) process.exitCode = 5;
|
|
223
|
+
})().catch((err) => die(err?.message || String(err)));
|