@whamp/pi-pstack 0.7.0
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/LICENSE +21 -0
- package/README.md +68 -0
- package/agents/comment-sicko.md +34 -0
- package/agents/poteto-agent.md +13 -0
- package/extensions/pstack/ask-user-question.ts +137 -0
- package/extensions/pstack/config.ts +55 -0
- package/extensions/pstack/index.ts +330 -0
- package/extensions/pstack/pstack-config-status.ts +72 -0
- package/extensions/pstack/pstack-role-config-store.ts +287 -0
- package/extensions/pstack/pstack-role-config.ts +496 -0
- package/extensions/pstack/pstack-role-prompt.ts +33 -0
- package/extensions/pstack/pstack-roles.ts +252 -0
- package/extensions/pstack/pstack-setup-plan.ts +223 -0
- package/extensions/pstack/skill-strip.ts +62 -0
- package/package.json +60 -0
- package/skills/architect/SKILL.md +83 -0
- package/skills/architect/references/design-red-flags.md +33 -0
- package/skills/architect/references/rationale-template.md +35 -0
- package/skills/architect/references/runner-prompt.md +20 -0
- package/skills/arena/SKILL.md +71 -0
- package/skills/automate-me/SKILL.md +104 -0
- package/skills/blast-radius/SKILL.md +50 -0
- package/skills/bro/SKILL.md +7 -0
- package/skills/create-verification-skill/SKILL.md +44 -0
- package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
- package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
- package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
- package/skills/deslop/SKILL.md +23 -0
- package/skills/figure-it-out/SKILL.md +53 -0
- package/skills/how/SKILL.md +52 -0
- package/skills/how/references/explainer-prompt.md +55 -0
- package/skills/how/references/explorer-prompt.md +52 -0
- package/skills/interrogate/SKILL.md +111 -0
- package/skills/interrogate/references/code-quality-review.md +47 -0
- package/skills/interrogate/references/lead-judgment.md +58 -0
- package/skills/interrogate/references/reviewer-prompt.md +72 -0
- package/skills/interrogate/references/rubric.md +77 -0
- package/skills/maintain-verification-skill/SKILL.md +39 -0
- package/skills/no-comments/SKILL.md +24 -0
- package/skills/poteto-mode/SKILL.md +147 -0
- package/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
- package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-stack.md +16 -0
- package/skills/poteto-mode/playbooks/babysit.md +27 -0
- package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
- package/skills/poteto-mode/playbooks/eval.md +25 -0
- package/skills/poteto-mode/playbooks/feature.md +21 -0
- package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
- package/skills/poteto-mode/playbooks/investigation.md +14 -0
- package/skills/poteto-mode/playbooks/multi-phase-plan.md +156 -0
- package/skills/poteto-mode/playbooks/opening-a-pr.md +33 -0
- package/skills/poteto-mode/playbooks/orchestrate.md +199 -0
- package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
- package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
- package/skills/poteto-mode/playbooks/prototype.md +14 -0
- package/skills/poteto-mode/playbooks/refactoring.md +16 -0
- package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
- package/skills/poteto-mode/playbooks/session-pickup.md +11 -0
- package/skills/poteto-mode/playbooks/shipping.md +17 -0
- package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
- package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
- package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
- package/skills/poteto-mode/references/bugbot-triage.md +142 -0
- package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
- package/skills/poteto-mode/scripts/bun.lock +67 -0
- package/skills/poteto-mode/scripts/check-plan.mjs +186 -0
- package/skills/poteto-mode/scripts/check-plan.test.mjs +25 -0
- package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
- package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
- package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
- package/skills/poteto-mode/scripts/package.json +16 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
- package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
- package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
- package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
- package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
- package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
- package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
- package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
- package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
- package/skills/poteto-mode/scripts/worktree-audit.sh +85 -0
- package/skills/principle-attack-the-premise/SKILL.md +23 -0
- package/skills/principle-boundary-discipline/SKILL.md +34 -0
- package/skills/principle-build-the-lever/SKILL.md +23 -0
- package/skills/principle-encode-lessons-in-structure/SKILL.md +31 -0
- package/skills/principle-exhaust-the-design-space/SKILL.md +21 -0
- package/skills/principle-experience-first/SKILL.md +19 -0
- package/skills/principle-fix-root-causes/SKILL.md +23 -0
- package/skills/principle-foundational-thinking/SKILL.md +21 -0
- package/skills/principle-guard-the-context-window/SKILL.md +17 -0
- package/skills/principle-laziness-protocol/SKILL.md +18 -0
- package/skills/principle-make-operations-idempotent/SKILL.md +24 -0
- package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +22 -0
- package/skills/principle-minimize-reader-load/SKILL.md +23 -0
- package/skills/principle-model-the-domain/SKILL.md +26 -0
- package/skills/principle-never-block-on-the-human/SKILL.md +22 -0
- package/skills/principle-outcome-oriented-execution/SKILL.md +22 -0
- package/skills/principle-prove-it-works/SKILL.md +33 -0
- package/skills/principle-redesign-from-first-principles/SKILL.md +16 -0
- package/skills/principle-separate-before-serializing-shared-state/SKILL.md +16 -0
- package/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
- package/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/skills/principle-test-behavior-not-implementation/SKILL.md +25 -0
- package/skills/principle-type-system-discipline/SKILL.md +31 -0
- package/skills/recall/SKILL.md +35 -0
- package/skills/reflect/SKILL.md +73 -0
- package/skills/reflect/references/divergent-reviewer.md +43 -0
- package/skills/reflect/references/judgment-reviewer.md +42 -0
- package/skills/reflect/references/synthesizer.md +56 -0
- package/skills/reflect/references/tooling-reviewer.md +55 -0
- package/skills/setup-pstack/SKILL.md +38 -0
- package/skills/setup-pstack/references/MODEL-ROLES.md +35 -0
- package/skills/show-me-your-work/SKILL.md +82 -0
- package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
- package/skills/show-me-your-work/scripts/log.sh +40 -0
- package/skills/swarm/SKILL.md +46 -0
- package/skills/tdd/SKILL.md +44 -0
- package/skills/teach/SKILL.md +21 -0
- package/skills/technical-writing/SKILL.md +127 -0
- package/skills/typescript-best-practices/SKILL.md +29 -0
- package/skills/typescript-best-practices/references/patterns.md +313 -0
- package/skills/unslop/SKILL.md +67 -0
- package/skills/why/SKILL.md +154 -0
- package/skills/why/references/epistemics.md +144 -0
- package/skills/why/references/investigator-prompt.md +103 -0
- package/skills/why/references/source-playbook.md +17 -0
- package/skills/why/references/sources/code-archaeology.md +88 -0
- package/skills/why/references/sources/databricks.md +70 -0
- package/skills/why/references/sources/datadog.md +99 -0
- package/skills/why/references/sources/incident-postmortem.md +15 -0
- package/skills/why/references/sources/linear.md +48 -0
- package/skills/why/references/sources/notion.md +55 -0
- package/skills/why/references/sources/sentry.md +100 -0
- package/skills/why/references/sources/slack.md +54 -0
- package/skills/why/references/synthesizer-prompt.md +135 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { isSafeModelSelector } from "./config.ts";
|
|
2
|
+
import {
|
|
3
|
+
PSTACK_LEGACY_ROLE_TARGETS,
|
|
4
|
+
PSTACK_ROLES,
|
|
5
|
+
RETIRED_PSTACK_ROLE_NAME,
|
|
6
|
+
isPstackRoleName,
|
|
7
|
+
type PstackModelSelector,
|
|
8
|
+
type PstackRoleConfig,
|
|
9
|
+
type PstackRoleName,
|
|
10
|
+
type PstackRoleSelections,
|
|
11
|
+
} from "./pstack-roles.ts";
|
|
12
|
+
|
|
13
|
+
const INHERIT_ALIASES = new Set(["inherit-parent", "auto"]);
|
|
14
|
+
|
|
15
|
+
/** Finite diagnostic codes for pstack role config decode and store. */
|
|
16
|
+
export const PSTACK_CONFIG_DIAGNOSTIC_CODES = [
|
|
17
|
+
"invalid-json",
|
|
18
|
+
"unsupported-version",
|
|
19
|
+
"invalid-document",
|
|
20
|
+
"invalid-role-selection",
|
|
21
|
+
"invalid-selector",
|
|
22
|
+
"unknown-role",
|
|
23
|
+
"retired-role",
|
|
24
|
+
"invalid-skills-enabled",
|
|
25
|
+
"ambiguous-legacy-selection",
|
|
26
|
+
"legacy-migrated",
|
|
27
|
+
"not-regular-file",
|
|
28
|
+
"config-too-large",
|
|
29
|
+
"config-read-failed",
|
|
30
|
+
"config-changed",
|
|
31
|
+
"config-write-failed",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/** Stable machine-readable reason for one config diagnostic. */
|
|
35
|
+
export type PstackConfigDiagnosticCode = (typeof PSTACK_CONFIG_DIAGNOSTIC_CODES)[number];
|
|
36
|
+
|
|
37
|
+
/** Structured config diagnostic. Codes are the finite PstackConfigDiagnosticCode union. */
|
|
38
|
+
export interface PstackConfigDiagnostic {
|
|
39
|
+
readonly code: PstackConfigDiagnosticCode;
|
|
40
|
+
readonly severity: "info" | "warning" | "error";
|
|
41
|
+
readonly message: string;
|
|
42
|
+
readonly role?: string;
|
|
43
|
+
readonly index?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Where a loaded pstack config came from. missing is normal. */
|
|
47
|
+
export type PstackConfigSource = "missing" | "v1" | "v2" | "markdown" | "invalid";
|
|
48
|
+
|
|
49
|
+
/** Loader/decoder result. Always includes a usable normalized v2 config. */
|
|
50
|
+
export interface PstackConfigReadResult {
|
|
51
|
+
readonly config: PstackRoleConfig;
|
|
52
|
+
readonly source: PstackConfigSource;
|
|
53
|
+
readonly diagnostics: readonly PstackConfigDiagnostic[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Typed success or failure of a v2 config write. */
|
|
57
|
+
export type PstackConfigWriteResult =
|
|
58
|
+
| { readonly ok: true; readonly path: string; readonly backupPath?: string }
|
|
59
|
+
| { readonly ok: false; readonly diagnostics: readonly PstackConfigDiagnostic[] };
|
|
60
|
+
|
|
61
|
+
/** Empty v2 config. Omitted roles inherit and skills stay enabled. */
|
|
62
|
+
export function defaultPstackRoleConfig(): PstackRoleConfig {
|
|
63
|
+
return { version: 2, roles: {}, skillsEnabled: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Build a pstack config diagnostic with a finite code. */
|
|
67
|
+
export function pstackConfigDiagnostic(
|
|
68
|
+
code: PstackConfigDiagnosticCode,
|
|
69
|
+
severity: PstackConfigDiagnostic["severity"],
|
|
70
|
+
message: string,
|
|
71
|
+
extra?: { role?: string; index?: number },
|
|
72
|
+
): PstackConfigDiagnostic {
|
|
73
|
+
return extra ? { code, severity, message, ...extra } : { code, severity, message };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isJsonRecord(value: unknown): value is Record<string, unknown> {
|
|
77
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function describeConfigPath(path?: string): string {
|
|
81
|
+
return path ?? "pstack models.json";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function brandModelSelector(value: string): PstackModelSelector {
|
|
85
|
+
// SAFETY: isSafeModelSelector already accepted this string.
|
|
86
|
+
return value as PstackModelSelector;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isListCardinality(name: PstackRoleName): boolean {
|
|
90
|
+
const cardinality = PSTACK_ROLES[name].cardinality;
|
|
91
|
+
return cardinality === "fanout" || cardinality === "pick-one";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseSkillsEnabledFlag(
|
|
95
|
+
raw: Record<string, unknown>,
|
|
96
|
+
diagnostics: PstackConfigDiagnostic[],
|
|
97
|
+
): boolean {
|
|
98
|
+
if (raw.skillsEnabled === undefined) return true;
|
|
99
|
+
if (raw.skillsEnabled === true || raw.skillsEnabled === false) return raw.skillsEnabled;
|
|
100
|
+
diagnostics.push(
|
|
101
|
+
pstackConfigDiagnostic(
|
|
102
|
+
"invalid-skills-enabled",
|
|
103
|
+
"warning",
|
|
104
|
+
"Invalid skillsEnabled flag. Defaulting to true.",
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function retiredRoleDiagnostic(role: string): PstackConfigDiagnostic {
|
|
111
|
+
return pstackConfigDiagnostic(
|
|
112
|
+
"retired-role",
|
|
113
|
+
"warning",
|
|
114
|
+
`Retired role "${role}" has no replacement and has no effect. Left the source unchanged.`,
|
|
115
|
+
{ role },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function assignNormalizedRole(
|
|
120
|
+
roles: Record<string, string | readonly string[]>,
|
|
121
|
+
name: PstackRoleName,
|
|
122
|
+
value: string | readonly string[],
|
|
123
|
+
): void {
|
|
124
|
+
roles[name] = value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function typedRoleSelections(
|
|
128
|
+
roles: Record<string, string | readonly string[]>,
|
|
129
|
+
): PstackRoleSelections {
|
|
130
|
+
// SAFETY: keys are registry role names and values passed cardinality checks.
|
|
131
|
+
return roles as PstackRoleSelections;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function assignSelectorToLegacyTargets(
|
|
135
|
+
targets: readonly PstackRoleName[],
|
|
136
|
+
selector: PstackModelSelector,
|
|
137
|
+
roles: Record<string, string | readonly string[]>,
|
|
138
|
+
): void {
|
|
139
|
+
for (const target of targets) {
|
|
140
|
+
if (isListCardinality(target)) assignNormalizedRole(roles, target, [selector]);
|
|
141
|
+
else assignNormalizedRole(roles, target, selector);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function migrateV1RoleValue(
|
|
146
|
+
key: string,
|
|
147
|
+
value: unknown,
|
|
148
|
+
targets: readonly PstackRoleName[],
|
|
149
|
+
roles: Record<string, string | readonly string[]>,
|
|
150
|
+
diagnostics: PstackConfigDiagnostic[],
|
|
151
|
+
): void {
|
|
152
|
+
if (typeof value === "string") {
|
|
153
|
+
if (INHERIT_ALIASES.has(value)) return;
|
|
154
|
+
if (!isSafeModelSelector(value)) {
|
|
155
|
+
diagnostics.push(
|
|
156
|
+
pstackConfigDiagnostic(
|
|
157
|
+
"invalid-selector",
|
|
158
|
+
"warning",
|
|
159
|
+
`Invalid selector for "${key}". That role inherits.`,
|
|
160
|
+
{ role: key },
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
assignSelectorToLegacyTargets(targets, brandModelSelector(value), roles);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!Array.isArray(value)) {
|
|
169
|
+
diagnostics.push(
|
|
170
|
+
pstackConfigDiagnostic(
|
|
171
|
+
"invalid-role-selection",
|
|
172
|
+
"warning",
|
|
173
|
+
`Invalid selection for "${key}". That role inherits.`,
|
|
174
|
+
{ role: key },
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const real: PstackModelSelector[] = [];
|
|
181
|
+
let inheritCount = 0;
|
|
182
|
+
for (let index = 0; index < value.length; index++) {
|
|
183
|
+
const item = value[index];
|
|
184
|
+
if (typeof item === "string" && INHERIT_ALIASES.has(item)) {
|
|
185
|
+
inheritCount += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (typeof item === "string" && isSafeModelSelector(item)) {
|
|
189
|
+
real.push(brandModelSelector(item));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
diagnostics.push(
|
|
193
|
+
pstackConfigDiagnostic(
|
|
194
|
+
"invalid-selector",
|
|
195
|
+
"warning",
|
|
196
|
+
`Invalid selector for "${key}" at index ${index}.`,
|
|
197
|
+
{ role: key, index },
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (inheritCount > 0 && real.length > 0) {
|
|
203
|
+
diagnostics.push(
|
|
204
|
+
pstackConfigDiagnostic(
|
|
205
|
+
"invalid-role-selection",
|
|
206
|
+
"info",
|
|
207
|
+
`Removed inherit-parent/auto from "${key}" because mixed v1 arrays never launched parent-model children.`,
|
|
208
|
+
{ role: key },
|
|
209
|
+
),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (real.length === 0) return;
|
|
213
|
+
|
|
214
|
+
const firstTarget = targets[0];
|
|
215
|
+
if (firstTarget !== undefined && isListCardinality(firstTarget)) {
|
|
216
|
+
for (const target of targets) assignNormalizedRole(roles, target, real);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (real.length > 1) {
|
|
221
|
+
diagnostics.push(
|
|
222
|
+
pstackConfigDiagnostic(
|
|
223
|
+
"ambiguous-legacy-selection",
|
|
224
|
+
"warning",
|
|
225
|
+
`Ambiguous v1 array for "${key}". Retained 1 selector and discarded ${real.length - 1}. Left the source unchanged.`,
|
|
226
|
+
{ role: key },
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const firstSelector = real[0];
|
|
231
|
+
if (firstSelector === undefined) return;
|
|
232
|
+
assignSelectorToLegacyTargets(targets, firstSelector, roles);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function migrateV1Document(raw: Record<string, unknown>, path?: string): PstackConfigReadResult {
|
|
236
|
+
const diagnostics: PstackConfigDiagnostic[] = [];
|
|
237
|
+
const skillsEnabled = parseSkillsEnabledFlag(raw, diagnostics);
|
|
238
|
+
if (!isJsonRecord(raw.roles)) {
|
|
239
|
+
diagnostics.push(
|
|
240
|
+
pstackConfigDiagnostic(
|
|
241
|
+
"invalid-document",
|
|
242
|
+
"error",
|
|
243
|
+
`Invalid roles object in ${describeConfigPath(path)}. Roles fell back to inherit-parent.`,
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
diagnostics.push(
|
|
247
|
+
pstackConfigDiagnostic(
|
|
248
|
+
"legacy-migrated",
|
|
249
|
+
"info",
|
|
250
|
+
`Migrated v1 config in ${describeConfigPath(path)} in memory. Source bytes were not rewritten.`,
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
return { config: { version: 2, roles: {}, skillsEnabled }, source: "v1", diagnostics };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const roles: Record<string, string | readonly string[]> = {};
|
|
257
|
+
for (const [key, value] of Object.entries(raw.roles)) {
|
|
258
|
+
if (key === RETIRED_PSTACK_ROLE_NAME) {
|
|
259
|
+
diagnostics.push(retiredRoleDiagnostic(key));
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const targets = PSTACK_LEGACY_ROLE_TARGETS.get(key);
|
|
263
|
+
if (!targets) {
|
|
264
|
+
diagnostics.push(
|
|
265
|
+
pstackConfigDiagnostic(
|
|
266
|
+
"unknown-role",
|
|
267
|
+
"warning",
|
|
268
|
+
`Unknown role "${key}" has no effect. Left the source unchanged.`,
|
|
269
|
+
{ role: key },
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
migrateV1RoleValue(key, value, targets, roles, diagnostics);
|
|
275
|
+
}
|
|
276
|
+
diagnostics.push(
|
|
277
|
+
pstackConfigDiagnostic(
|
|
278
|
+
"legacy-migrated",
|
|
279
|
+
"info",
|
|
280
|
+
`Migrated v1 config in ${describeConfigPath(path)} in memory. Source bytes were not rewritten.`,
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
return {
|
|
284
|
+
config: { version: 2, roles: typedRoleSelections(roles), skillsEnabled },
|
|
285
|
+
source: "v1",
|
|
286
|
+
diagnostics,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function decodeV2RoleValue(
|
|
291
|
+
role: PstackRoleName,
|
|
292
|
+
value: unknown,
|
|
293
|
+
diagnostics: PstackConfigDiagnostic[],
|
|
294
|
+
): string | readonly string[] | undefined {
|
|
295
|
+
const list = isListCardinality(role);
|
|
296
|
+
const cardinality = PSTACK_ROLES[role].cardinality;
|
|
297
|
+
|
|
298
|
+
if (typeof value === "string") {
|
|
299
|
+
if (INHERIT_ALIASES.has(value)) return undefined;
|
|
300
|
+
if (!isSafeModelSelector(value)) {
|
|
301
|
+
diagnostics.push(
|
|
302
|
+
pstackConfigDiagnostic(
|
|
303
|
+
"invalid-selector",
|
|
304
|
+
"error",
|
|
305
|
+
`Invalid selector for "${role}". That role inherits.`,
|
|
306
|
+
{ role },
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
if (!list) return brandModelSelector(value);
|
|
312
|
+
diagnostics.push(
|
|
313
|
+
pstackConfigDiagnostic(
|
|
314
|
+
"invalid-role-selection",
|
|
315
|
+
"error",
|
|
316
|
+
`Role "${role}" is ${cardinality} and requires a non-empty selector array. That role inherits.`,
|
|
317
|
+
{ role },
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!Array.isArray(value)) {
|
|
324
|
+
diagnostics.push(
|
|
325
|
+
pstackConfigDiagnostic(
|
|
326
|
+
"invalid-role-selection",
|
|
327
|
+
"error",
|
|
328
|
+
`Invalid selection for "${role}". That role inherits.`,
|
|
329
|
+
{ role },
|
|
330
|
+
),
|
|
331
|
+
);
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!list) {
|
|
336
|
+
diagnostics.push(
|
|
337
|
+
pstackConfigDiagnostic(
|
|
338
|
+
"invalid-role-selection",
|
|
339
|
+
"error",
|
|
340
|
+
`Role "${role}" is ${cardinality} and requires one selector. That role inherits.`,
|
|
341
|
+
{ role },
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (value.length === 0) {
|
|
348
|
+
diagnostics.push(
|
|
349
|
+
pstackConfigDiagnostic(
|
|
350
|
+
"invalid-role-selection",
|
|
351
|
+
"error",
|
|
352
|
+
`Role "${role}" requires a non-empty selector array. That role inherits.`,
|
|
353
|
+
{ role },
|
|
354
|
+
),
|
|
355
|
+
);
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const selectors: PstackModelSelector[] = [];
|
|
360
|
+
for (let index = 0; index < value.length; index++) {
|
|
361
|
+
const item = value[index];
|
|
362
|
+
if (typeof item === "string" && INHERIT_ALIASES.has(item)) {
|
|
363
|
+
diagnostics.push(
|
|
364
|
+
pstackConfigDiagnostic(
|
|
365
|
+
"invalid-role-selection",
|
|
366
|
+
"error",
|
|
367
|
+
`Inheritance aliases inside arrays are invalid for "${role}". That role inherits.`,
|
|
368
|
+
{ role, index },
|
|
369
|
+
),
|
|
370
|
+
);
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
if (typeof item !== "string" || !isSafeModelSelector(item)) {
|
|
374
|
+
diagnostics.push(
|
|
375
|
+
pstackConfigDiagnostic(
|
|
376
|
+
"invalid-selector",
|
|
377
|
+
"error",
|
|
378
|
+
`Invalid selector for "${role}" at index ${index}. That role inherits.`,
|
|
379
|
+
{ role, index },
|
|
380
|
+
),
|
|
381
|
+
);
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
selectors.push(brandModelSelector(item));
|
|
385
|
+
}
|
|
386
|
+
return selectors;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function decodeV2Document(raw: Record<string, unknown>, path?: string): PstackConfigReadResult {
|
|
390
|
+
const diagnostics: PstackConfigDiagnostic[] = [];
|
|
391
|
+
const skillsEnabled = parseSkillsEnabledFlag(raw, diagnostics);
|
|
392
|
+
if (!isJsonRecord(raw.roles)) {
|
|
393
|
+
diagnostics.push(
|
|
394
|
+
pstackConfigDiagnostic(
|
|
395
|
+
"invalid-document",
|
|
396
|
+
"error",
|
|
397
|
+
`Invalid roles object in ${describeConfigPath(path)}. Roles fell back to inherit-parent.`,
|
|
398
|
+
),
|
|
399
|
+
);
|
|
400
|
+
return { config: { version: 2, roles: {}, skillsEnabled }, source: "v2", diagnostics };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const roles: Record<string, string | readonly string[]> = {};
|
|
404
|
+
for (const [key, value] of Object.entries(raw.roles)) {
|
|
405
|
+
if (key === RETIRED_PSTACK_ROLE_NAME) {
|
|
406
|
+
diagnostics.push(retiredRoleDiagnostic(key));
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (!isPstackRoleName(key)) {
|
|
410
|
+
diagnostics.push(
|
|
411
|
+
pstackConfigDiagnostic("unknown-role", "warning", `Unknown role "${key}" has no effect.`, {
|
|
412
|
+
role: key,
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const decoded = decodeV2RoleValue(key, value, diagnostics);
|
|
418
|
+
if (decoded !== undefined) assignNormalizedRole(roles, key, decoded);
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
config: { version: 2, roles: typedRoleSelections(roles), skillsEnabled },
|
|
422
|
+
source: "v2",
|
|
423
|
+
diagnostics,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function invalidPstackConfig(
|
|
428
|
+
code: "invalid-document" | "invalid-json" | "unsupported-version",
|
|
429
|
+
message: string,
|
|
430
|
+
): PstackConfigReadResult {
|
|
431
|
+
return {
|
|
432
|
+
config: defaultPstackRoleConfig(),
|
|
433
|
+
source: "invalid",
|
|
434
|
+
diagnostics: [pstackConfigDiagnostic(code, "error", message)],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function decodePstackConfigValue(raw: unknown, path?: string): PstackConfigReadResult {
|
|
439
|
+
if (!isJsonRecord(raw)) {
|
|
440
|
+
return invalidPstackConfig(
|
|
441
|
+
"invalid-document",
|
|
442
|
+
`Invalid document in ${describeConfigPath(path)}. Using default inherited roles.`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
if (raw.version === 1) return migrateV1Document(raw, path);
|
|
446
|
+
if (raw.version === 2) return decodeV2Document(raw, path);
|
|
447
|
+
if (raw.version === undefined) {
|
|
448
|
+
return invalidPstackConfig(
|
|
449
|
+
"invalid-document",
|
|
450
|
+
`Invalid document in ${describeConfigPath(path)}. Using default inherited roles.`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return invalidPstackConfig(
|
|
454
|
+
"unsupported-version",
|
|
455
|
+
`Unsupported version in ${describeConfigPath(path)}. Using default inherited roles.`,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Decode pstack JSON text into a normalized v2 config. Never throws. */
|
|
460
|
+
export function decodePstackConfigText(text: string, path?: string): PstackConfigReadResult {
|
|
461
|
+
try {
|
|
462
|
+
return decodePstackConfigValue(JSON.parse(text), path);
|
|
463
|
+
} catch {
|
|
464
|
+
return invalidPstackConfig(
|
|
465
|
+
"invalid-json",
|
|
466
|
+
`Invalid JSON in ${describeConfigPath(path)}. Using default inherited roles.`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function parseLegacyMarkdownRoles(text: string): Record<string, unknown> {
|
|
472
|
+
const roles: Record<string, unknown> = Object.create(null);
|
|
473
|
+
for (const line of text.split(/\r?\n/)) {
|
|
474
|
+
const trimmed = line.trim();
|
|
475
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
476
|
+
const colon = trimmed.indexOf(":");
|
|
477
|
+
if (colon <= 0) continue;
|
|
478
|
+
const name = trimmed.slice(0, colon).trim();
|
|
479
|
+
if (!name) continue;
|
|
480
|
+
const parts = trimmed
|
|
481
|
+
.slice(colon + 1)
|
|
482
|
+
.split(",")
|
|
483
|
+
.map((part) => part.trim())
|
|
484
|
+
.filter(Boolean);
|
|
485
|
+
roles[name] = parts.length <= 1 ? (parts[0] ?? "") : parts;
|
|
486
|
+
}
|
|
487
|
+
return roles;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Decode legacy markdown role lines through the v1 migration boundary. Never throws. */
|
|
491
|
+
export function decodePstackLegacyMarkdownText(
|
|
492
|
+
text: string,
|
|
493
|
+
path?: string,
|
|
494
|
+
): PstackConfigReadResult {
|
|
495
|
+
return decodePstackConfigValue({ version: 1, roles: parseLegacyMarkdownRoles(text) }, path);
|
|
496
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PSTACK_INHERIT_PARENT,
|
|
3
|
+
PSTACK_ROLE_NAMES,
|
|
4
|
+
PSTACK_ROLES,
|
|
5
|
+
type PstackRoleConfig,
|
|
6
|
+
} from "./pstack-roles.ts";
|
|
7
|
+
|
|
8
|
+
const POTETO_PROMPT =
|
|
9
|
+
"New task? Playbook match or rigor needed -> apply /poteto-mode. Casual turn or user opts out -> don't.";
|
|
10
|
+
|
|
11
|
+
const ADVISORY =
|
|
12
|
+
"Pstack model roles. These are advisory model selections; tools and authority are separate.";
|
|
13
|
+
|
|
14
|
+
/** Render configured pstack role lines with cardinality. Inherit-all is empty. */
|
|
15
|
+
export function formatPstackRoleTable(config: PstackRoleConfig): string {
|
|
16
|
+
const lines: string[] = [];
|
|
17
|
+
for (const role of PSTACK_ROLE_NAMES) {
|
|
18
|
+
const value = config.roles[role];
|
|
19
|
+
if (value === undefined || value === PSTACK_INHERIT_PARENT) continue;
|
|
20
|
+
lines.push(`${role} [${PSTACK_ROLES[role].cardinality}]: ${JSON.stringify(value)}`);
|
|
21
|
+
}
|
|
22
|
+
if (lines.length === 0) return "";
|
|
23
|
+
return [ADVISORY, ...lines].join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Assemble the extra system prompt from role table and optional Poteto Mode. */
|
|
27
|
+
export function systemPromptInjection(config: PstackRoleConfig, potetoMode: boolean): string {
|
|
28
|
+
const parts: string[] = [];
|
|
29
|
+
const table = formatPstackRoleTable(config);
|
|
30
|
+
if (table) parts.push(table);
|
|
31
|
+
if (potetoMode) parts.push(POTETO_PROMPT);
|
|
32
|
+
return parts.join("\n\n");
|
|
33
|
+
}
|