pi-amq 0.1.1
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 +66 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/THIRD_PARTY_NOTICES.md +33 -0
- package/dist/extension/guidance.d.ts +25 -0
- package/dist/extension/guidance.d.ts.map +1 -0
- package/dist/extension/guidance.js +121 -0
- package/dist/extension/guidance.js.map +1 -0
- package/dist/extension/guide.d.ts +30 -0
- package/dist/extension/guide.d.ts.map +1 -0
- package/dist/extension/guide.js +128 -0
- package/dist/extension/guide.js.map +1 -0
- package/dist/extension/index.d.ts +4 -0
- package/dist/extension/index.d.ts.map +1 -0
- package/dist/extension/index.js +6 -0
- package/dist/extension/index.js.map +1 -0
- package/dist/extension/runtime.d.ts +48 -0
- package/dist/extension/runtime.d.ts.map +1 -0
- package/dist/extension/runtime.js +1095 -0
- package/dist/extension/runtime.js.map +1 -0
- package/dist/extension/tools.d.ts +12 -0
- package/dist/extension/tools.d.ts.map +1 -0
- package/dist/extension/tools.js +198 -0
- package/dist/extension/tools.js.map +1 -0
- package/dist/extension/worker-runtime.d.ts +34 -0
- package/dist/extension/worker-runtime.d.ts.map +1 -0
- package/dist/extension/worker-runtime.js +974 -0
- package/dist/extension/worker-runtime.js.map +1 -0
- package/dist/extension/worker-tools.d.ts +140 -0
- package/dist/extension/worker-tools.d.ts.map +1 -0
- package/dist/extension/worker-tools.js +217 -0
- package/dist/extension/worker-tools.js.map +1 -0
- package/dist/integration/confirmed-bind-onboarding.d.ts +44 -0
- package/dist/integration/confirmed-bind-onboarding.d.ts.map +1 -0
- package/dist/integration/confirmed-bind-onboarding.js +199 -0
- package/dist/integration/confirmed-bind-onboarding.js.map +1 -0
- package/dist/integration/fleet-view.d.ts +66 -0
- package/dist/integration/fleet-view.d.ts.map +1 -0
- package/dist/integration/fleet-view.js +238 -0
- package/dist/integration/fleet-view.js.map +1 -0
- package/dist/integration/fresh-session-dispatcher.d.ts +66 -0
- package/dist/integration/fresh-session-dispatcher.d.ts.map +1 -0
- package/dist/integration/fresh-session-dispatcher.js +103 -0
- package/dist/integration/fresh-session-dispatcher.js.map +1 -0
- package/dist/integration/launch-preview.d.ts +31 -0
- package/dist/integration/launch-preview.d.ts.map +1 -0
- package/dist/integration/launch-preview.js +325 -0
- package/dist/integration/launch-preview.js.map +1 -0
- package/dist/integration/transport-port.d.ts +39 -0
- package/dist/integration/transport-port.d.ts.map +1 -0
- package/dist/integration/transport-port.js +2 -0
- package/dist/integration/transport-port.js.map +1 -0
- package/dist/integration/worker-completion-notifier.d.ts +22 -0
- package/dist/integration/worker-completion-notifier.d.ts.map +1 -0
- package/dist/integration/worker-completion-notifier.js +81 -0
- package/dist/integration/worker-completion-notifier.js.map +1 -0
- package/dist/transport/amq-cli-adapter.d.ts +41 -0
- package/dist/transport/amq-cli-adapter.d.ts.map +1 -0
- package/dist/transport/amq-cli-adapter.js +291 -0
- package/dist/transport/amq-cli-adapter.js.map +1 -0
- package/dist/transport/amq-message.d.ts +47 -0
- package/dist/transport/amq-message.d.ts.map +1 -0
- package/dist/transport/amq-message.js +173 -0
- package/dist/transport/amq-message.js.map +1 -0
- package/dist/transport/amq-operations.d.ts +105 -0
- package/dist/transport/amq-operations.d.ts.map +1 -0
- package/dist/transport/amq-operations.js +320 -0
- package/dist/transport/amq-operations.js.map +1 -0
- package/dist/transport/atomic-json.d.ts +18 -0
- package/dist/transport/atomic-json.d.ts.map +1 -0
- package/dist/transport/atomic-json.js +90 -0
- package/dist/transport/atomic-json.js.map +1 -0
- package/dist/transport/binding-resolver.d.ts +46 -0
- package/dist/transport/binding-resolver.d.ts.map +1 -0
- package/dist/transport/binding-resolver.js +389 -0
- package/dist/transport/binding-resolver.js.map +1 -0
- package/dist/transport/bridge-journal.d.ts +100 -0
- package/dist/transport/bridge-journal.d.ts.map +1 -0
- package/dist/transport/bridge-journal.js +459 -0
- package/dist/transport/bridge-journal.js.map +1 -0
- package/dist/transport/dlq-retry-audit.d.ts +28 -0
- package/dist/transport/dlq-retry-audit.d.ts.map +1 -0
- package/dist/transport/dlq-retry-audit.js +66 -0
- package/dist/transport/dlq-retry-audit.js.map +1 -0
- package/dist/transport/inbox-pump.d.ts +81 -0
- package/dist/transport/inbox-pump.d.ts.map +1 -0
- package/dist/transport/inbox-pump.js +558 -0
- package/dist/transport/inbox-pump.js.map +1 -0
- package/dist/transport/index.d.ts +12 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +12 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/presence-projector.d.ts +29 -0
- package/dist/transport/presence-projector.d.ts.map +1 -0
- package/dist/transport/presence-projector.js +73 -0
- package/dist/transport/presence-projector.js.map +1 -0
- package/dist/transport/priority-delivery-scheduler.d.ts +85 -0
- package/dist/transport/priority-delivery-scheduler.d.ts.map +1 -0
- package/dist/transport/priority-delivery-scheduler.js +210 -0
- package/dist/transport/priority-delivery-scheduler.js.map +1 -0
- package/dist/transport/receiver-lease.d.ts +102 -0
- package/dist/transport/receiver-lease.d.ts.map +1 -0
- package/dist/transport/receiver-lease.js +778 -0
- package/dist/transport/receiver-lease.js.map +1 -0
- package/dist/transport/runtime-status.d.ts +48 -0
- package/dist/transport/runtime-status.d.ts.map +1 -0
- package/dist/transport/runtime-status.js +71 -0
- package/dist/transport/runtime-status.js.map +1 -0
- package/dist/workflow/atomic-storage.d.ts +12 -0
- package/dist/workflow/atomic-storage.d.ts.map +1 -0
- package/dist/workflow/atomic-storage.js +104 -0
- package/dist/workflow/atomic-storage.js.map +1 -0
- package/dist/workflow/budget.d.ts +18 -0
- package/dist/workflow/budget.d.ts.map +1 -0
- package/dist/workflow/budget.js +74 -0
- package/dist/workflow/budget.js.map +1 -0
- package/dist/workflow/index.d.ts +17 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +17 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/lifecycle-projection.d.ts +18 -0
- package/dist/workflow/lifecycle-projection.d.ts.map +1 -0
- package/dist/workflow/lifecycle-projection.js +22 -0
- package/dist/workflow/lifecycle-projection.js.map +1 -0
- package/dist/workflow/model-fallback.d.ts +2 -0
- package/dist/workflow/model-fallback.d.ts.map +1 -0
- package/dist/workflow/model-fallback.js +17 -0
- package/dist/workflow/model-fallback.js.map +1 -0
- package/dist/workflow/pi-session-fork.d.ts +18 -0
- package/dist/workflow/pi-session-fork.d.ts.map +1 -0
- package/dist/workflow/pi-session-fork.js +80 -0
- package/dist/workflow/pi-session-fork.js.map +1 -0
- package/dist/workflow/run-artifacts.d.ts +5 -0
- package/dist/workflow/run-artifacts.d.ts.map +1 -0
- package/dist/workflow/run-artifacts.js +25 -0
- package/dist/workflow/run-artifacts.js.map +1 -0
- package/dist/workflow/run-cleanup.d.ts +70 -0
- package/dist/workflow/run-cleanup.d.ts.map +1 -0
- package/dist/workflow/run-cleanup.js +222 -0
- package/dist/workflow/run-cleanup.js.map +1 -0
- package/dist/workflow/run-gate.d.ts +43 -0
- package/dist/workflow/run-gate.d.ts.map +1 -0
- package/dist/workflow/run-gate.js +70 -0
- package/dist/workflow/run-gate.js.map +1 -0
- package/dist/workflow/run-store.d.ts +208 -0
- package/dist/workflow/run-store.d.ts.map +1 -0
- package/dist/workflow/run-store.js +2965 -0
- package/dist/workflow/run-store.js.map +1 -0
- package/dist/workflow/spawn-confirmation.d.ts +44 -0
- package/dist/workflow/spawn-confirmation.d.ts.map +1 -0
- package/dist/workflow/spawn-confirmation.js +228 -0
- package/dist/workflow/spawn-confirmation.js.map +1 -0
- package/dist/workflow/system-process-inspector.d.ts +9 -0
- package/dist/workflow/system-process-inspector.d.ts.map +1 -0
- package/dist/workflow/system-process-inspector.js +129 -0
- package/dist/workflow/system-process-inspector.js.map +1 -0
- package/dist/workflow/worker-child-extension.d.ts +3 -0
- package/dist/workflow/worker-child-extension.d.ts.map +1 -0
- package/dist/workflow/worker-child-extension.js +182 -0
- package/dist/workflow/worker-child-extension.js.map +1 -0
- package/dist/workflow/worker-gate.d.ts +74 -0
- package/dist/workflow/worker-gate.d.ts.map +1 -0
- package/dist/workflow/worker-gate.js +165 -0
- package/dist/workflow/worker-gate.js.map +1 -0
- package/dist/workflow/worker-launch.d.ts +65 -0
- package/dist/workflow/worker-launch.d.ts.map +1 -0
- package/dist/workflow/worker-launch.js +338 -0
- package/dist/workflow/worker-launch.js.map +1 -0
- package/dist/workflow/worker-model-catalog.d.ts +36 -0
- package/dist/workflow/worker-model-catalog.d.ts.map +1 -0
- package/dist/workflow/worker-model-catalog.js +105 -0
- package/dist/workflow/worker-model-catalog.js.map +1 -0
- package/dist/workflow/worker-process-supervisor.d.ts +110 -0
- package/dist/workflow/worker-process-supervisor.d.ts.map +1 -0
- package/dist/workflow/worker-process-supervisor.js +1188 -0
- package/dist/workflow/worker-process-supervisor.js.map +1 -0
- package/dist/workflow/worker-prompt-capsule.d.ts +32 -0
- package/dist/workflow/worker-prompt-capsule.d.ts.map +1 -0
- package/dist/workflow/worker-prompt-capsule.js +52 -0
- package/dist/workflow/worker-prompt-capsule.js.map +1 -0
- package/dist/workflow/worker-role-presets.d.ts +38 -0
- package/dist/workflow/worker-role-presets.d.ts.map +1 -0
- package/dist/workflow/worker-role-presets.js +201 -0
- package/dist/workflow/worker-role-presets.js.map +1 -0
- package/dist/workflow/workflow-doctor.d.ts +57 -0
- package/dist/workflow/workflow-doctor.d.ts.map +1 -0
- package/dist/workflow/workflow-doctor.js +289 -0
- package/dist/workflow/workflow-doctor.js.map +1 -0
- package/dist/workflow/workflow-lease.d.ts +125 -0
- package/dist/workflow/workflow-lease.d.ts.map +1 -0
- package/dist/workflow/workflow-lease.js +689 -0
- package/dist/workflow/workflow-lease.js.map +1 -0
- package/dist/workflow/workflow-types.d.ts +335 -0
- package/dist/workflow/workflow-types.d.ts.map +1 -0
- package/dist/workflow/workflow-types.js +11 -0
- package/dist/workflow/workflow-types.js.map +1 -0
- package/dist/workflow/workspace-paths.d.ts +4 -0
- package/dist/workflow/workspace-paths.d.ts.map +1 -0
- package/dist/workflow/workspace-paths.js +22 -0
- package/dist/workflow/workspace-paths.js.map +1 -0
- package/docs/conformance.md +116 -0
- package/docs/demo-transcript.md +34 -0
- package/docs/design.md +1481 -0
- package/docs/operator-guide.md +100 -0
- package/extension/guidance.ts +162 -0
- package/extension/guide.ts +155 -0
- package/extension/index.ts +8 -0
- package/extension/runtime.ts +1224 -0
- package/extension/tools.ts +221 -0
- package/extension/worker-runtime.ts +1150 -0
- package/extension/worker-tools.ts +276 -0
- package/integration/confirmed-bind-onboarding.ts +296 -0
- package/integration/fleet-view.ts +286 -0
- package/integration/fresh-session-dispatcher.ts +166 -0
- package/integration/launch-preview.ts +396 -0
- package/integration/transport-port.ts +29 -0
- package/integration/worker-completion-notifier.ts +98 -0
- package/package.json +55 -0
- package/roles/oracle.md +9 -0
- package/roles/researcher.md +8 -0
- package/roles/reviewer.md +9 -0
- package/roles/scout.md +8 -0
- package/roles/worker.md +10 -0
- package/scripts/demo-two-terminal.sh +59 -0
- package/skills/coordination-patterns/SKILL.md +41 -0
- package/skills/coordination-patterns/references/supervised-worker-fan-out.md +12 -0
- package/skills/coordination-patterns/references/today-mail-team.md +36 -0
- package/transport/amq-cli-adapter.ts +381 -0
- package/transport/amq-message.ts +218 -0
- package/transport/amq-operations.ts +482 -0
- package/transport/atomic-json.ts +115 -0
- package/transport/binding-resolver.ts +565 -0
- package/transport/bridge-journal.ts +588 -0
- package/transport/dlq-retry-audit.ts +97 -0
- package/transport/inbox-pump.ts +729 -0
- package/transport/index.ts +11 -0
- package/transport/presence-projector.ts +104 -0
- package/transport/priority-delivery-scheduler.ts +328 -0
- package/transport/receiver-lease.ts +945 -0
- package/transport/runtime-status.ts +112 -0
- package/workflow/.gitkeep +1 -0
- package/workflow/atomic-storage.ts +120 -0
- package/workflow/budget.ts +90 -0
- package/workflow/index.ts +16 -0
- package/workflow/lifecycle-projection.ts +37 -0
- package/workflow/model-fallback.ts +17 -0
- package/workflow/pi-session-fork.ts +96 -0
- package/workflow/run-artifacts.ts +36 -0
- package/workflow/run-cleanup.ts +315 -0
- package/workflow/run-gate.ts +122 -0
- package/workflow/run-store.ts +3890 -0
- package/workflow/spawn-confirmation.ts +305 -0
- package/workflow/system-process-inspector.ts +142 -0
- package/workflow/worker-child-extension.ts +193 -0
- package/workflow/worker-gate.ts +238 -0
- package/workflow/worker-launch.ts +485 -0
- package/workflow/worker-model-catalog.ts +165 -0
- package/workflow/worker-process-supervisor.ts +1504 -0
- package/workflow/worker-prompt-capsule.ts +81 -0
- package/workflow/worker-role-presets.ts +241 -0
- package/workflow/workflow-doctor.ts +400 -0
- package/workflow/workflow-lease.ts +905 -0
- package/workflow/workflow-types.ts +367 -0
- package/workflow/workspace-paths.ts +32 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
interface WorkerPromptContract {
|
|
2
|
+
readonly goal: string;
|
|
3
|
+
readonly acceptance: readonly string[];
|
|
4
|
+
readonly constraints: readonly string[];
|
|
5
|
+
readonly role?: {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly tier: string;
|
|
8
|
+
readonly charter: string;
|
|
9
|
+
};
|
|
10
|
+
readonly continuation?: {
|
|
11
|
+
readonly priorWorkerId: string;
|
|
12
|
+
readonly priorAttemptId: string;
|
|
13
|
+
readonly sessionFile: string;
|
|
14
|
+
};
|
|
15
|
+
readonly context?: "fresh" | "fork";
|
|
16
|
+
readonly forkSource?: {
|
|
17
|
+
readonly sourceSessionFile: string;
|
|
18
|
+
readonly branchPointId: string;
|
|
19
|
+
readonly inheritedEntries: number;
|
|
20
|
+
readonly inheritedBytes: number;
|
|
21
|
+
};
|
|
22
|
+
readonly contextRefs?: readonly {
|
|
23
|
+
readonly kind: "file" | "session" | "artifact";
|
|
24
|
+
readonly value: string;
|
|
25
|
+
}[];
|
|
26
|
+
readonly capabilities?: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function canonicalWorkerCapsule(contract: WorkerPromptContract): string {
|
|
30
|
+
const artifactDirectories = contract.contextRefs
|
|
31
|
+
?.filter((reference) => reference.kind === "artifact")
|
|
32
|
+
.map((reference) => reference.value) ?? [];
|
|
33
|
+
const peerGrants = contract.capabilities
|
|
34
|
+
?.filter((capability) => capability.startsWith("message_worker:")) ?? [];
|
|
35
|
+
return [
|
|
36
|
+
"<worker-contract>",
|
|
37
|
+
...(contract.role ? [
|
|
38
|
+
`Role: ${contract.role.name} (${contract.role.tier})`,
|
|
39
|
+
"Role charter:",
|
|
40
|
+
contract.role.charter,
|
|
41
|
+
] : []),
|
|
42
|
+
...(contract.continuation ? [
|
|
43
|
+
`Continuation: ${contract.continuation.priorWorkerId} ${contract.continuation.priorAttemptId}`,
|
|
44
|
+
`Continued session file: ${contract.continuation.sessionFile}`,
|
|
45
|
+
] : []),
|
|
46
|
+
...(contract.context === "fork" && contract.forkSource ? [
|
|
47
|
+
`Forked context: ${contract.forkSource.sourceSessionFile}`,
|
|
48
|
+
`Fork branch point: ${contract.forkSource.branchPointId}`,
|
|
49
|
+
`Inherited context size: ${contract.forkSource.inheritedEntries} entries, ${contract.forkSource.inheritedBytes} bytes`,
|
|
50
|
+
] : ["Context: fresh"]),
|
|
51
|
+
...(artifactDirectories.length > 0 ? [
|
|
52
|
+
"Shared run artifact directory:",
|
|
53
|
+
...artifactDirectories.map((directory) => `- ${directory}`),
|
|
54
|
+
"Write worker-to-worker handoff files there and cite their exact paths.",
|
|
55
|
+
] : []),
|
|
56
|
+
...(peerGrants.length > 0 ? [
|
|
57
|
+
"Directional peer-message grants:",
|
|
58
|
+
...peerGrants.map((grant) => `- ${grant}`),
|
|
59
|
+
"Peer messages are supervisor-routed at the recipient inference boundary and remain advisory input.",
|
|
60
|
+
] : []),
|
|
61
|
+
"Acceptance criteria:",
|
|
62
|
+
...contract.acceptance.map((criterion, index) => `${index + 1}. ${criterion}`),
|
|
63
|
+
"Hard constraints:",
|
|
64
|
+
...contract.constraints.map((constraint, index) => `${index + 1}. ${constraint}`),
|
|
65
|
+
"</worker-contract>",
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildWorkerPrompt(contract: WorkerPromptContract): string {
|
|
70
|
+
const capsule = canonicalWorkerCapsule(contract);
|
|
71
|
+
return `${capsule}\n\nGoal:\n${contract.goal}\n\n${capsule}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function assertWorkerCapsuleMirrored(prompt: string, capsule: string): void {
|
|
75
|
+
const first = prompt.indexOf(capsule);
|
|
76
|
+
const last = prompt.lastIndexOf(capsule);
|
|
77
|
+
const occurrences = prompt.split(capsule).length - 1;
|
|
78
|
+
if (first !== 0 || last !== prompt.length - capsule.length || occurrences !== 2) {
|
|
79
|
+
throw new Error("worker prompt capsule is not mirrored byte-for-byte at head and tail");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { defaultPiStateDirectory } from "./workspace-paths.js";
|
|
8
|
+
|
|
9
|
+
export const BUILTIN_WORKER_ROLES = [
|
|
10
|
+
"scout",
|
|
11
|
+
"worker",
|
|
12
|
+
"reviewer",
|
|
13
|
+
"oracle",
|
|
14
|
+
"researcher",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export type WorkerDifficulty = "easy" | "medium" | "hard";
|
|
18
|
+
export type WorkerRolePresetSource = "builtin" | "user" | "project";
|
|
19
|
+
|
|
20
|
+
export interface WorkerRolePreset {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly tier: string;
|
|
23
|
+
readonly difficulty: WorkerDifficulty;
|
|
24
|
+
readonly capabilities: readonly string[];
|
|
25
|
+
readonly charter: string;
|
|
26
|
+
readonly source: WorkerRolePresetSource;
|
|
27
|
+
readonly fingerprint: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WorkerRoleContract {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly tier: string;
|
|
33
|
+
readonly charter: string;
|
|
34
|
+
readonly presetSource: WorkerRolePresetSource;
|
|
35
|
+
readonly presetFingerprint: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WorkerRolePresetCatalogOptions {
|
|
39
|
+
readonly projectDirectory: string;
|
|
40
|
+
readonly projectTrusted: boolean;
|
|
41
|
+
readonly builtinDirectory?: string;
|
|
42
|
+
readonly userDirectory?: string;
|
|
43
|
+
readonly projectRolesDirectory?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class WorkerRolePresetCatalog {
|
|
47
|
+
readonly #presets: ReadonlyMap<string, WorkerRolePreset>;
|
|
48
|
+
|
|
49
|
+
private constructor(presets: ReadonlyMap<string, WorkerRolePreset>) {
|
|
50
|
+
this.#presets = presets;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static async load(options: WorkerRolePresetCatalogOptions): Promise<WorkerRolePresetCatalog> {
|
|
54
|
+
const builtinDirectory = options.builtinDirectory ?? defaultBuiltinRoleDirectory();
|
|
55
|
+
const userDirectory = options.userDirectory
|
|
56
|
+
?? join(defaultPiStateDirectory(), "pi-amq", "roles");
|
|
57
|
+
const projectDirectory = options.projectRolesDirectory
|
|
58
|
+
?? join(options.projectDirectory, ".pi", "pi-amq", "roles");
|
|
59
|
+
const layered = new Map<string, WorkerRolePreset>();
|
|
60
|
+
installLayer(layered, await readRoleDirectory(builtinDirectory, "builtin", true));
|
|
61
|
+
for (const required of BUILTIN_WORKER_ROLES) {
|
|
62
|
+
if (!layered.has(required)) {
|
|
63
|
+
throw new Error(`Installed role preset directory is missing ${required}.md`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
installLayer(layered, await readRoleDirectory(userDirectory, "user", false));
|
|
67
|
+
if (options.projectTrusted) {
|
|
68
|
+
installLayer(layered, await readRoleDirectory(projectDirectory, "project", false));
|
|
69
|
+
}
|
|
70
|
+
return new WorkerRolePresetCatalog(layered);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
resolve(requestedRole: string | undefined): WorkerRolePreset {
|
|
74
|
+
const normalized = normalizeRoleName(requestedRole ?? "worker");
|
|
75
|
+
const preset = this.#presets.get(normalized);
|
|
76
|
+
if (!preset) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Unknown worker role ${JSON.stringify(requestedRole)}; available roles: ${this.names().join(", ")}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return preset;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
names(): readonly string[] {
|
|
85
|
+
return [...this.#presets.keys()].sort();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
presets(): readonly WorkerRolePreset[] {
|
|
89
|
+
return this.names().map((name) => this.#presets.get(name)!);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function parseWorkerRolePreset(
|
|
94
|
+
markdown: string,
|
|
95
|
+
source: WorkerRolePresetSource,
|
|
96
|
+
sourceName: string,
|
|
97
|
+
): WorkerRolePreset {
|
|
98
|
+
const normalized = markdown.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
|
|
99
|
+
const lines = normalized.split("\n");
|
|
100
|
+
if (lines[0] !== "---") throw new TypeError(`${sourceName}: role preset must start with ---`);
|
|
101
|
+
const closing = lines.indexOf("---", 1);
|
|
102
|
+
if (closing < 0) throw new TypeError(`${sourceName}: role preset frontmatter is not closed`);
|
|
103
|
+
const frontmatter = lines.slice(1, closing);
|
|
104
|
+
const charter = lines.slice(closing + 1).join("\n").trim();
|
|
105
|
+
if (charter.length === 0) throw new TypeError(`${sourceName}: role charter must not be empty`);
|
|
106
|
+
|
|
107
|
+
const values = new Map<string, string | string[]>();
|
|
108
|
+
let activeList: string | undefined;
|
|
109
|
+
for (let index = 0; index < frontmatter.length; index += 1) {
|
|
110
|
+
const raw = frontmatter[index]!;
|
|
111
|
+
if (raw.trim().length === 0) continue;
|
|
112
|
+
const listItem = raw.match(/^\s+-\s+(.+?)\s*$/);
|
|
113
|
+
if (listItem) {
|
|
114
|
+
if (!activeList) throw new TypeError(`${sourceName}:${index + 2}: list item has no field`);
|
|
115
|
+
const current = values.get(activeList);
|
|
116
|
+
if (!Array.isArray(current)) throw new TypeError(`${sourceName}:${index + 2}: invalid list`);
|
|
117
|
+
current.push(unquote(listItem[1]!));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const scalar = raw.match(/^([a-z_]+):(?:\s*(.*))?$/);
|
|
121
|
+
if (!scalar) throw new TypeError(`${sourceName}:${index + 2}: unsupported frontmatter syntax`);
|
|
122
|
+
const key = scalar[1]!;
|
|
123
|
+
if (!ROLE_FIELDS.has(key)) throw new TypeError(`${sourceName}: unknown role field ${key}`);
|
|
124
|
+
if (values.has(key)) throw new TypeError(`${sourceName}: duplicate role field ${key}`);
|
|
125
|
+
const value = scalar[2]?.trim() ?? "";
|
|
126
|
+
if (key === "capabilities") {
|
|
127
|
+
if (value.length > 0) throw new TypeError(`${sourceName}: capabilities must be a YAML list`);
|
|
128
|
+
values.set(key, []);
|
|
129
|
+
activeList = key;
|
|
130
|
+
} else {
|
|
131
|
+
if (value.length === 0) throw new TypeError(`${sourceName}: ${key} must not be empty`);
|
|
132
|
+
values.set(key, unquote(value));
|
|
133
|
+
activeList = undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const field of ROLE_FIELDS) {
|
|
137
|
+
if (!values.has(field)) throw new TypeError(`${sourceName}: missing role field ${field}`);
|
|
138
|
+
}
|
|
139
|
+
const name = requiredScalar(values, "name", sourceName);
|
|
140
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
141
|
+
throw new TypeError(`${sourceName}: role name must match [a-z][a-z0-9-]*`);
|
|
142
|
+
}
|
|
143
|
+
const tier = requiredScalar(values, "tier", sourceName);
|
|
144
|
+
const difficulty = requiredScalar(values, "difficulty", sourceName);
|
|
145
|
+
if (difficulty !== "easy" && difficulty !== "medium" && difficulty !== "hard") {
|
|
146
|
+
throw new TypeError(`${sourceName}: difficulty must be easy, medium, or hard`);
|
|
147
|
+
}
|
|
148
|
+
const capabilities = values.get("capabilities");
|
|
149
|
+
if (!Array.isArray(capabilities) || capabilities.length === 0) {
|
|
150
|
+
throw new TypeError(`${sourceName}: capabilities must contain at least one grant`);
|
|
151
|
+
}
|
|
152
|
+
const uniqueCapabilities = [...new Set(capabilities.map((value) => requireText(value, sourceName)))];
|
|
153
|
+
return {
|
|
154
|
+
name,
|
|
155
|
+
tier,
|
|
156
|
+
difficulty,
|
|
157
|
+
capabilities: uniqueCapabilities,
|
|
158
|
+
charter,
|
|
159
|
+
source,
|
|
160
|
+
fingerprint: createHash("sha256").update(normalized).digest("hex"),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function workerRoleContract(preset: WorkerRolePreset): WorkerRoleContract {
|
|
165
|
+
return {
|
|
166
|
+
name: preset.name,
|
|
167
|
+
tier: preset.tier,
|
|
168
|
+
charter: preset.charter,
|
|
169
|
+
presetSource: preset.source,
|
|
170
|
+
presetFingerprint: preset.fingerprint,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function normalizeRoleName(role: string): string {
|
|
175
|
+
const normalized = role.trim().toLowerCase();
|
|
176
|
+
if (normalized === "implementer") return "worker";
|
|
177
|
+
if (normalized === "auditor") return "reviewer";
|
|
178
|
+
return normalized;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function readRoleDirectory(
|
|
182
|
+
directory: string,
|
|
183
|
+
source: WorkerRolePresetSource,
|
|
184
|
+
required: boolean,
|
|
185
|
+
): Promise<readonly WorkerRolePreset[]> {
|
|
186
|
+
let entries: string[];
|
|
187
|
+
try {
|
|
188
|
+
entries = await readdir(directory);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (!required && isMissing(error)) return [];
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
const presets: WorkerRolePreset[] = [];
|
|
194
|
+
const names = new Set<string>();
|
|
195
|
+
for (const entry of entries.filter((name) => name.endsWith(".md")).sort()) {
|
|
196
|
+
const path = join(directory, entry);
|
|
197
|
+
const preset = parseWorkerRolePreset(await readFile(path, "utf8"), source, path);
|
|
198
|
+
if (names.has(preset.name)) throw new TypeError(`${directory}: duplicate role ${preset.name}`);
|
|
199
|
+
names.add(preset.name);
|
|
200
|
+
presets.push(preset);
|
|
201
|
+
}
|
|
202
|
+
return presets;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function installLayer(
|
|
206
|
+
target: Map<string, WorkerRolePreset>,
|
|
207
|
+
presets: readonly WorkerRolePreset[],
|
|
208
|
+
): void {
|
|
209
|
+
for (const preset of presets) target.set(preset.name, preset);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function defaultBuiltinRoleDirectory(): string {
|
|
213
|
+
const adjacent = fileURLToPath(new URL("../roles/", import.meta.url));
|
|
214
|
+
return existsSync(adjacent)
|
|
215
|
+
? adjacent
|
|
216
|
+
: fileURLToPath(new URL("../../roles/", import.meta.url));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function requiredScalar(values: ReadonlyMap<string, string | string[]>, key: string, source: string): string {
|
|
220
|
+
const value = values.get(key);
|
|
221
|
+
if (typeof value !== "string") throw new TypeError(`${source}: ${key} must be a scalar`);
|
|
222
|
+
return requireText(value, source);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function requireText(value: string, source: string): string {
|
|
226
|
+
const trimmed = value.trim();
|
|
227
|
+
if (trimmed.length === 0) throw new TypeError(`${source}: empty role field value`);
|
|
228
|
+
return trimmed;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function unquote(value: string): string {
|
|
232
|
+
const quoted = value.match(/^(?:"([\s\S]*)"|'([\s\S]*)')$/);
|
|
233
|
+
return quoted ? (quoted[1] ?? quoted[2] ?? "") : value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isMissing(error: unknown): boolean {
|
|
237
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
238
|
+
&& (error as { readonly code?: unknown }).code === "ENOENT";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const ROLE_FIELDS = new Set(["name", "tier", "difficulty", "capabilities"]);
|