@fro.bot/systematic 3.4.0 → 3.5.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.
@@ -16206,6 +16206,58 @@ var CategoryOverlaySchema = exports_external.object({
16206
16206
  description: "Per-category configuration overlay (same fields as agent minus disable)",
16207
16207
  examples: [{ model: "anthropic/claude-opus-4-7", temperature: 0.1 }]
16208
16208
  });
16209
+ var piSubagentsThinkingSchema = exports_external.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]).meta({
16210
+ description: "pi-subagents reasoning effort level for exported persona frontmatter",
16211
+ examples: ["off", "medium", "high"]
16212
+ });
16213
+ var piSubagentsMaxTurnsSchema = exports_external.number().int().nonnegative().meta({
16214
+ description: "pi-subagents maximum turns for a delegated persona (0 = unlimited)",
16215
+ examples: [0, 10, 25]
16216
+ });
16217
+ var piSubagentsToolsSchema = exports_external.string().min(1).meta({
16218
+ description: "pi-subagents comma-selector tool string (built-ins, */all/none, or extension selectors)",
16219
+ examples: ["*", "read,grep,glob", "none"]
16220
+ });
16221
+ var piSubagentsSkillsSchema = exports_external.union([exports_external.literal(true), exports_external.string().min(1)]).meta({
16222
+ description: "pi-subagents skills selector: true (all) or a comma-separated list of skill names",
16223
+ examples: [true, "ce:plan,ce:review"]
16224
+ });
16225
+ var PiSubagentsAgentOverlaySchema = exports_external.object({
16226
+ thinking: trustProtected(piSubagentsThinkingSchema).optional(),
16227
+ max_turns: trustAny(piSubagentsMaxTurnsSchema).optional(),
16228
+ tools: trustProtected(piSubagentsToolsSchema).optional(),
16229
+ skills: trustProtected(piSubagentsSkillsSchema).optional()
16230
+ }).strict().meta({
16231
+ description: "Per-agent pi-subagents export overlay (Pi-native fields only; no model)",
16232
+ examples: [{ thinking: "high", max_turns: 10 }]
16233
+ });
16234
+ var PiSubagentsCategoryOverlaySchema = PiSubagentsAgentOverlaySchema.meta({
16235
+ description: "Per-category pi-subagents export overlay (Pi-native fields only; no model)",
16236
+ examples: [{ thinking: "medium" }]
16237
+ });
16238
+ var PiSubagentsSchema = exports_external.object({
16239
+ categories: exports_external.record(exports_external.string(), PiSubagentsCategoryOverlaySchema).default({}).meta({
16240
+ description: "Per-category pi-subagents export overlays keyed by category name",
16241
+ examples: [{ research: { thinking: "high" } }, {}]
16242
+ }),
16243
+ agents: exports_external.record(exports_external.string(), PiSubagentsAgentOverlaySchema).default({}).meta({
16244
+ description: "Per-agent pi-subagents export overlays keyed by bundled agent name",
16245
+ examples: [{ "repo-research-analyst": { max_turns: 10 } }, {}]
16246
+ })
16247
+ }).strict().default({ categories: {}, agents: {} }).meta({
16248
+ description: "Pi-native pi-subagents export field overlays (thinking, max_turns, tools, skills). Category values apply first; per-agent values override. No model field \u2014 model stays in the categories/agents overlay.",
16249
+ examples: [
16250
+ {
16251
+ categories: { research: { thinking: "high" } },
16252
+ agents: { "repo-research-analyst": { max_turns: 10 } }
16253
+ }
16254
+ ]
16255
+ });
16256
+ var PI_SUBAGENTS_PROTECTED_FIELDS = [
16257
+ "thinking",
16258
+ "tools",
16259
+ "skills"
16260
+ ];
16209
16261
  var BootstrapSchema = exports_external.object({
16210
16262
  enabled: exports_external.boolean().default(true).meta({
16211
16263
  description: "Enable bootstrap prompt injection into every conversation",
@@ -16276,6 +16328,7 @@ function createSystematicConfigSchema(opts) {
16276
16328
  ]
16277
16329
  }),
16278
16330
  workflow_guard: WorkflowGuardSchema,
16331
+ pi_subagents: PiSubagentsSchema,
16279
16332
  skills_as_commands: exports_external.boolean().default(true).meta({
16280
16333
  description: "Register skills discovered from user/project skill directories (OpenCode config and other agent-harness-standard locations) as slash commands. Default true.",
16281
16334
  examples: [true, false]
@@ -16315,6 +16368,7 @@ var DEFAULT_CONFIG = {
16315
16368
  },
16316
16369
  agents: {},
16317
16370
  categories: {},
16371
+ pi_subagents: { categories: {}, agents: {} },
16318
16372
  skills_as_commands: true
16319
16373
  };
16320
16374
  var SECURITY_OVERLAY_FIELDS2 = new Set(SECURITY_OVERLAY_FIELDS);
@@ -16473,16 +16527,18 @@ function mergeArraysUnique(arr1, arr2) {
16473
16527
  set2.add(item);
16474
16528
  return Array.from(set2);
16475
16529
  }
16476
- function loadConfig(projectDir) {
16477
- return loadConfigWithSources(projectDir).config;
16530
+ function loadConfig(projectDir, options) {
16531
+ return loadConfigWithSources(projectDir, options).config;
16478
16532
  }
16479
- function loadConfigWithSources(projectDir) {
16533
+ function loadConfigWithSources(projectDir, options) {
16534
+ const includeProject = options?.includeProject ?? true;
16480
16535
  const paths = getConfigPaths(projectDir);
16481
16536
  const userSource = loadConfigSource(paths.userConfig, "user");
16482
- const projectSource = loadConfigSource(paths.projectConfig, "project");
16537
+ const projectSource = includeProject ? loadConfigSource(paths.projectConfig, "project") : null;
16483
16538
  const customSource = paths.customConfig ? loadConfigSource(paths.customConfig, "custom") : null;
16484
16539
  const sources = [userSource, projectSource, customSource].filter((source) => source !== null);
16485
16540
  const mergedOverlays = mergeOverlaySources(sources);
16541
+ const mergedPiSubagentsOverlays = mergePiSubagentsOverlaySources(sources);
16486
16542
  const droppedCategories = Object.keys(mergedOverlays.categories).filter((name) => REMOVED_AGENT_CATEGORIES_SET.has(name));
16487
16543
  const warned = new Set;
16488
16544
  warnDroppedNames(droppedCategories, "categories", warned, "v3.0.0");
@@ -16511,6 +16567,10 @@ function loadConfigWithSources(projectDir) {
16511
16567
  },
16512
16568
  agents: overlayValues(overlays.agents),
16513
16569
  categories: overlayValues(overlays.categories),
16570
+ pi_subagents: {
16571
+ categories: overlayValues(mergedPiSubagentsOverlays.categories),
16572
+ agents: overlayValues(mergedPiSubagentsOverlays.agents)
16573
+ },
16514
16574
  skills_as_commands: customConfig?.skills_as_commands ?? projectConfig?.skills_as_commands ?? userConfig?.skills_as_commands ?? DEFAULT_CONFIG.skills_as_commands
16515
16575
  };
16516
16576
  const droppedSkills = computeDroppedNames(result.disabled_skills, CURRENT_SKILL_NAMES_SET);
@@ -16577,6 +16637,57 @@ function preserveSecurityFields(previous, next) {
16577
16637
  }
16578
16638
  return result;
16579
16639
  }
16640
+ var PI_SUBAGENTS_PROTECTED_FIELD_SET = new Set(PI_SUBAGENTS_PROTECTED_FIELDS);
16641
+ function mergePiSubagentsOverlaySources(sources) {
16642
+ const result = {
16643
+ agents: {},
16644
+ categories: {}
16645
+ };
16646
+ for (const source of sources) {
16647
+ mergePiSubagentsOverlayMap(result.agents, source, "agents");
16648
+ mergePiSubagentsOverlayMap(result.categories, source, "categories");
16649
+ }
16650
+ return result;
16651
+ }
16652
+ function stripPiSubagentsProtectedFields(value) {
16653
+ const result = {};
16654
+ for (const [field, fieldValue] of Object.entries(value)) {
16655
+ if (PI_SUBAGENTS_PROTECTED_FIELD_SET.has(field))
16656
+ continue;
16657
+ result[field] = fieldValue;
16658
+ }
16659
+ return result;
16660
+ }
16661
+ function preservePiSubagentsProtectedFields(previous, next) {
16662
+ const result = { ...next };
16663
+ for (const field of PI_SUBAGENTS_PROTECTED_FIELD_SET) {
16664
+ if (Object.hasOwn(previous, field)) {
16665
+ result[field] = previous[field];
16666
+ }
16667
+ }
16668
+ return result;
16669
+ }
16670
+ function mergePiSubagentsOverlayMap(target, source, mapKey) {
16671
+ const overlayMap = source.config.pi_subagents?.[mapKey];
16672
+ if (overlayMap === undefined)
16673
+ return;
16674
+ if (!isRecord2(overlayMap)) {
16675
+ throwInvalidOverlay(source.path, `pi_subagents.${mapKey}`);
16676
+ }
16677
+ for (const [key, rawValue] of Object.entries(overlayMap)) {
16678
+ const keyPath = `pi_subagents.${mapKey}.${key}`;
16679
+ if (!isRecord2(rawValue)) {
16680
+ throwInvalidOverlay(source.path, keyPath);
16681
+ }
16682
+ const previous = target[key];
16683
+ const value = source.trust === "project" ? preservePiSubagentsProtectedFields(previous?.value ?? {}, stripPiSubagentsProtectedFields(rawValue)) : rawValue;
16684
+ target[key] = {
16685
+ value,
16686
+ sourcePath: source.path,
16687
+ keyPath
16688
+ };
16689
+ }
16690
+ }
16580
16691
  function overlayValues(overlays) {
16581
16692
  const result = {};
16582
16693
  for (const [key, overlay] of Object.entries(overlays)) {
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  loadConfigWithSources,
16
16
  parseFrontmatter,
17
17
  walkDir
18
- } from "./index-0vm17gwv.js";
18
+ } from "./index-1mb4baxr.js";
19
19
 
20
20
  // src/index.ts
21
21
  import { createHash as createHash5 } from "crypto";
@@ -2890,6 +2890,19 @@ function seedFromMarker(marker) {
2890
2890
  capabilityFlags: marker.capabilityFlags
2891
2891
  };
2892
2892
  }
2893
+ function registrationDigestFromMarker(marker) {
2894
+ if (marker.kind === "mint")
2895
+ return marker.envelope.registrationDigest;
2896
+ return marker.registrationDigest;
2897
+ }
2898
+ function filterMarkersByRegistration(markers, ownRegistrationDigest) {
2899
+ return markers.filter((input) => {
2900
+ const validation = validateReceiptMarker(input);
2901
+ if (validation.status !== "valid")
2902
+ return true;
2903
+ return registrationDigestFromMarker(validation.marker) === ownRegistrationDigest;
2904
+ });
2905
+ }
2893
2906
  function extractReceiptReadbackSeed(inputs) {
2894
2907
  if (inputs.length === 0)
2895
2908
  return { status: "empty" };
@@ -10400,6 +10413,11 @@ var statusToolSchema = exports_external.object(statusToolShape).strict();
10400
10413
  function isRecord7(value) {
10401
10414
  return typeof value === "object" && value !== null && !Array.isArray(value);
10402
10415
  }
10416
+ function decodeSessionSaltBytes(hex) {
10417
+ if (typeof hex !== "string" || hex.length !== 64 || !/^[0-9a-f]+$/.test(hex))
10418
+ return;
10419
+ return Uint8Array.from(Buffer.from(hex, "hex"));
10420
+ }
10403
10421
  function boundedString(value, maxLength) {
10404
10422
  return typeof value === "string" && value.length > 0 && value.length <= maxLength;
10405
10423
  }
@@ -10979,6 +10997,46 @@ function createSessionRuntime(options) {
10979
10997
  function seedAgrees(nextLedger, seed) {
10980
10998
  return nextLedger.metadata.registrationDigest === seed.registrationDigest && JSON.stringify(nextLedger.metadata.capabilityFlags) === JSON.stringify(["workflow-guard"]);
10981
10999
  }
11000
+ function candidateSeedFromMarker(input) {
11001
+ const validation = validateReceiptMarker(input);
11002
+ if (validation.status !== "valid")
11003
+ return;
11004
+ const marker = validation.marker;
11005
+ if (marker.kind === "mint") {
11006
+ const salt = decodeSessionSaltBytes(marker.sessionSalt);
11007
+ return salt ? { salt, digest: marker.envelope.registrationDigest } : undefined;
11008
+ }
11009
+ if (marker.kind === "control" && marker.control === "progression") {
11010
+ const salt = decodeSessionSaltBytes(marker.sessionSalt);
11011
+ return salt ? { salt, digest: marker.registrationDigest } : undefined;
11012
+ }
11013
+ return;
11014
+ }
11015
+ function candidateAgreesWithOwnIdentity(salt, digest2) {
11016
+ try {
11017
+ const probe = createReceiptLedger({
11018
+ capabilityFlags: ["workflow-guard"],
11019
+ registrationIdentity: options.registrationIdentity,
11020
+ sessionSalt: salt
11021
+ });
11022
+ return probe.metadata.registrationDigest === digest2;
11023
+ } catch {
11024
+ return false;
11025
+ }
11026
+ }
11027
+ function resolveOwnRegistrationDigest(markers) {
11028
+ const seenDigests = new Set;
11029
+ for (const input of markers) {
11030
+ const candidate = candidateSeedFromMarker(input);
11031
+ if (!candidate || seenDigests.has(candidate.digest))
11032
+ continue;
11033
+ seenDigests.add(candidate.digest);
11034
+ if (candidateAgreesWithOwnIdentity(candidate.salt, candidate.digest)) {
11035
+ return candidate.digest;
11036
+ }
11037
+ }
11038
+ return;
11039
+ }
10982
11040
  function recoverPersistedMarkers(markers) {
10983
11041
  const seed = extractReceiptReadbackSeed(markers);
10984
11042
  if (seed.status !== "ready")
@@ -11017,6 +11075,39 @@ function createSessionRuntime(options) {
11017
11075
  initialized = true;
11018
11076
  return true;
11019
11077
  }
11078
+ function classifyMarkersFromParts(parts2) {
11079
+ const allMarkers = [];
11080
+ for (const part of parts2)
11081
+ collectReceiptMarkers(part, allMarkers);
11082
+ if (allMarkers.length === 0)
11083
+ return { kind: "foreign-empty" };
11084
+ const ownDigest = resolveOwnRegistrationDigest(allMarkers);
11085
+ if (ownDigest !== undefined) {
11086
+ return {
11087
+ kind: "own",
11088
+ markers: filterMarkersByRegistration(allMarkers, ownDigest)
11089
+ };
11090
+ }
11091
+ for (const input of allMarkers) {
11092
+ const validation = validateReceiptMarker(input);
11093
+ if (validation.status !== "valid") {
11094
+ return { kind: "ambiguous" };
11095
+ }
11096
+ const candidate = candidateSeedFromMarker(input);
11097
+ if (candidate === undefined) {
11098
+ return { kind: "ambiguous" };
11099
+ }
11100
+ }
11101
+ return { kind: "foreign-empty" };
11102
+ }
11103
+ function publishEmptyMarkers(parts2, allowFresh) {
11104
+ if (allowFresh && !parts2.some((part) => containsGuardHistory(part))) {
11105
+ publishFresh();
11106
+ } else {
11107
+ retryableEmptyHistory = true;
11108
+ publishUnavailable();
11109
+ }
11110
+ }
11020
11111
  async function initializeSession(sessionID, allowFresh) {
11021
11112
  const reader = options.hostReadback?.readSessionParts;
11022
11113
  if (!reader) {
@@ -11030,19 +11121,16 @@ function createSessionRuntime(options) {
11030
11121
  publishUnavailable();
11031
11122
  return;
11032
11123
  }
11033
- const markers = [];
11034
- for (const part of parts2)
11035
- collectReceiptMarkers(part, markers);
11036
- if (markers.length === 0) {
11037
- if (allowFresh && !parts2.some((part) => containsGuardHistory(part))) {
11038
- publishFresh();
11039
- } else {
11040
- retryableEmptyHistory = true;
11041
- publishUnavailable();
11042
- }
11124
+ const classification = classifyMarkersFromParts(parts2);
11125
+ if (classification.kind === "ambiguous") {
11126
+ publishUnavailable();
11043
11127
  return;
11044
11128
  }
11045
- if (!recoverPersistedMarkers(markers))
11129
+ if (classification.kind === "foreign-empty") {
11130
+ publishEmptyMarkers(parts2, allowFresh);
11131
+ return;
11132
+ }
11133
+ if (!recoverPersistedMarkers(classification.markers))
11046
11134
  publishUnavailable();
11047
11135
  }
11048
11136
  async function recoverFromHost(sessionID, allowFresh = true) {
@@ -11080,12 +11168,15 @@ function createSessionRuntime(options) {
11080
11168
  markUnavailable();
11081
11169
  return;
11082
11170
  }
11083
- const markers = [];
11084
- for (const part of parts2)
11085
- collectReceiptMarkers(part, markers);
11086
- if (markers.length === 0)
11171
+ const classification = classifyMarkersFromParts(parts2);
11172
+ if (classification.kind === "foreign-empty")
11087
11173
  return;
11088
- const seed = extractReceiptReadbackSeed(markers);
11174
+ if (classification.kind === "ambiguous") {
11175
+ markUnavailable();
11176
+ return;
11177
+ }
11178
+ const ownMarkers = classification.markers;
11179
+ const seed = extractReceiptReadbackSeed(ownMarkers);
11089
11180
  if (seed.status !== "ready") {
11090
11181
  markUnavailable();
11091
11182
  return;
@@ -11099,7 +11190,7 @@ function createSessionRuntime(options) {
11099
11190
  markUnavailable();
11100
11191
  return;
11101
11192
  }
11102
- const recovered = childLedger.recoverReadback(markers);
11193
+ const recovered = childLedger.recoverReadback(ownMarkers);
11103
11194
  if (recovered.status === "rejected") {
11104
11195
  markUnavailable();
11105
11196
  return;
@@ -87,6 +87,72 @@ export declare const CategoryOverlaySchema: z.ZodObject<{
87
87
  deny: "deny";
88
88
  }>>]>>>;
89
89
  }, z.core.$strict>;
90
+ export declare const PiSubagentsAgentOverlaySchema: z.ZodObject<{
91
+ thinking: z.ZodOptional<z.ZodEnum<{
92
+ off: "off";
93
+ minimal: "minimal";
94
+ low: "low";
95
+ medium: "medium";
96
+ high: "high";
97
+ xhigh: "xhigh";
98
+ max: "max";
99
+ }>>;
100
+ max_turns: z.ZodOptional<z.ZodNumber>;
101
+ tools: z.ZodOptional<z.ZodString>;
102
+ skills: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodString]>>;
103
+ }, z.core.$strict>;
104
+ export declare const PiSubagentsCategoryOverlaySchema: z.ZodObject<{
105
+ thinking: z.ZodOptional<z.ZodEnum<{
106
+ off: "off";
107
+ minimal: "minimal";
108
+ low: "low";
109
+ medium: "medium";
110
+ high: "high";
111
+ xhigh: "xhigh";
112
+ max: "max";
113
+ }>>;
114
+ max_turns: z.ZodOptional<z.ZodNumber>;
115
+ tools: z.ZodOptional<z.ZodString>;
116
+ skills: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodString]>>;
117
+ }, z.core.$strict>;
118
+ export declare const PiSubagentsSchema: z.ZodDefault<z.ZodObject<{
119
+ categories: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
120
+ thinking: z.ZodOptional<z.ZodEnum<{
121
+ off: "off";
122
+ minimal: "minimal";
123
+ low: "low";
124
+ medium: "medium";
125
+ high: "high";
126
+ xhigh: "xhigh";
127
+ max: "max";
128
+ }>>;
129
+ max_turns: z.ZodOptional<z.ZodNumber>;
130
+ tools: z.ZodOptional<z.ZodString>;
131
+ skills: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodString]>>;
132
+ }, z.core.$strict>>>;
133
+ agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
134
+ thinking: z.ZodOptional<z.ZodEnum<{
135
+ off: "off";
136
+ minimal: "minimal";
137
+ low: "low";
138
+ medium: "medium";
139
+ high: "high";
140
+ xhigh: "xhigh";
141
+ max: "max";
142
+ }>>;
143
+ max_turns: z.ZodOptional<z.ZodNumber>;
144
+ tools: z.ZodOptional<z.ZodString>;
145
+ skills: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodString]>>;
146
+ }, z.core.$strict>>>;
147
+ }, z.core.$strict>>;
148
+ /**
149
+ * Fields in PiSubagentsAgentOverlaySchema/PiSubagentsCategoryOverlaySchema that
150
+ * require a project-or-higher trust source. Project config cannot grant
151
+ * `thinking`, `tools`, or `skills` to an exported persona; `max_turns` is
152
+ * trust-any. Mirrors the hand-coded `PI_SUBAGENTS_PROTECTED_FIELDS` set in
153
+ * `src/lib/config.ts`.
154
+ */
155
+ export declare const PI_SUBAGENTS_PROTECTED_FIELDS: readonly string[];
90
156
  export declare const BootstrapSchema: z.ZodObject<{
91
157
  enabled: z.ZodDefault<z.ZodBoolean>;
92
158
  file: z.ZodOptional<z.ZodString>;
@@ -22,6 +22,10 @@ export interface SourceAwareConfigResult {
22
22
  config: SystematicConfig;
23
23
  overlays: SourcedOverlayConfigMap;
24
24
  }
25
+ export interface PiSubagentsOverlayMap {
26
+ categories?: OverlayConfigMap;
27
+ agents?: OverlayConfigMap;
28
+ }
25
29
  export interface SystematicConfig {
26
30
  disabled_skills: string[];
27
31
  disabled_agents: string[];
@@ -30,6 +34,7 @@ export interface SystematicConfig {
30
34
  workflow_guard: WorkflowGuardConfig;
31
35
  agents?: OverlayConfigMap;
32
36
  categories?: OverlayConfigMap;
37
+ pi_subagents?: PiSubagentsOverlayMap;
33
38
  skills_as_commands: boolean;
34
39
  }
35
40
  export declare const DEFAULT_CONFIG: SystematicConfig;
@@ -47,8 +52,17 @@ export declare function computeDroppedNames(names: readonly string[], allowedSet
47
52
  * Passing a fresh set per load ensures no cross-load suppression.
48
53
  */
49
54
  export declare function warnDroppedNames(dropped: string[], field: string, warned: Set<string>, removalVersion?: string): void;
50
- export declare function loadConfig(projectDir: string): SystematicConfig;
51
- export declare function loadConfigWithSources(projectDir: string): SourceAwareConfigResult;
55
+ export interface LoadConfigOptions {
56
+ /**
57
+ * When false, the project-level config source (`<cwd>/.opencode/systematic.json`)
58
+ * is not loaded at all — not merged, not trust-stripped, entirely absent from
59
+ * the source chain. Used by global-scoped pi-subagents export so it never
60
+ * absorbs cwd project overlays (plan R7/R19). Defaults to true.
61
+ */
62
+ includeProject?: boolean;
63
+ }
64
+ export declare function loadConfig(projectDir: string, options?: LoadConfigOptions): SystematicConfig;
65
+ export declare function loadConfigWithSources(projectDir: string, options?: LoadConfigOptions): SourceAwareConfigResult;
52
66
  export declare function getConfigPaths(projectDir: string): {
53
67
  customConfig?: string | undefined;
54
68
  customDir?: string | undefined;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * pi-subagents export lifecycle: resolve, preview, export, refresh, cleanup.
3
+ *
4
+ * Writes user-chosen agents dirs ($PI_CODING_AGENT_DIR/agents or
5
+ * <cwd>/.pi/agents). Batch-transactional with rollback: if any file write or
6
+ * the manifest write fails, the operation rolls back to the pre-operation
7
+ * state. Rollback reports any restoration failures explicitly.
8
+ *
9
+ * Manifest tracks ownership. Malformed or hostile manifests cause every
10
+ * lifecycle verb to refuse before mutation. No writes from module import.
11
+ */
12
+ export type ExportScope = 'project' | 'global';
13
+ /**
14
+ * Scope-appropriate config resolution for export/preview/refresh. When
15
+ * provided, the effective config (`user → project → custom` for project
16
+ * scope; `user → custom` for global scope — never absorbing cwd project
17
+ * overlays) is applied to each exported persona's frontmatter: `model`
18
+ * resolved from the `categories`/`agents` overlay (per-agent beats category;
19
+ * `model: null` omits) and Pi-native `pi_subagents` fields (`thinking`,
20
+ * `max_turns`, `tools`, `skills`) resolved from the `pi_subagents`
21
+ * namespace after trust filtering. Omitting `configOptions` preserves the
22
+ * model-free, config-neutral export (backward compatible default).
23
+ */
24
+ export interface ExportConfigOptions {
25
+ scope: ExportScope;
26
+ cwd: string;
27
+ }
28
+ export declare const MANIFEST_FILENAME = ".systematic-personas.json";
29
+ /** Exclusive per-root mutation lock. Guards export/refresh/cleanup; preview is lock-free. */
30
+ export declare const LOCK_FILENAME = ".systematic-personas.lock";
31
+ export interface ManifestFileEntry {
32
+ filename: string;
33
+ hash: string;
34
+ status: 'exported' | 'exported-with-warning';
35
+ }
36
+ export interface PiSubagentsManifest {
37
+ generatedAt: string;
38
+ agentsRoot: string;
39
+ files: ManifestFileEntry[];
40
+ }
41
+ /**
42
+ * Strict manifest read result — distinguishes absent from malformed.
43
+ *
44
+ * absent → no manifest file; operations proceed as first-export.
45
+ * ok → valid manifest, returned in `manifest`.
46
+ * malformed → manifest file exists but is invalid (bad JSON, wrong schema,
47
+ * duplicate filenames, unsafe filenames). Operations must refuse.
48
+ */
49
+ export type ManifestReadResult = {
50
+ kind: 'absent';
51
+ } | {
52
+ kind: 'ok';
53
+ manifest: PiSubagentsManifest;
54
+ } | {
55
+ kind: 'malformed';
56
+ error: string;
57
+ };
58
+ export declare function resolveAgentsRoot(scope: ExportScope, cwd: string): string;
59
+ /**
60
+ * Resolve the safety anchor for a scope: the topmost directory whose
61
+ * descendants (down to agentsRoot) are walked and lstat-checked for
62
+ * symlinks/non-directories. Never inspects ancestors above this anchor
63
+ * (avoids false positives from OS-level ancestor symlinks, e.g. macOS
64
+ * `/var` -> `/private/var`).
65
+ *
66
+ * - project: cwd
67
+ * - global with PI_CODING_AGENT_DIR set: the env dir's PARENT (so the env
68
+ * dir itself is included in the walk and checked)
69
+ * - global without PI_CODING_AGENT_DIR: homedir
70
+ */
71
+ export declare function resolveAnchor(scope: ExportScope, cwd: string): string;
72
+ export declare function readManifestStrict(agentsRoot: string): ManifestReadResult;
73
+ /**
74
+ * Convenience wrapper: returns the manifest for 'ok', null for 'absent', throws
75
+ * a structured Error for 'malformed'. Used by callers that already distinguished absent.
76
+ */
77
+ export declare function readManifest(agentsRoot: string): PiSubagentsManifest | null;
78
+ export declare function writeManifest(agentsRoot: string, manifest: PiSubagentsManifest): void;
79
+ export interface TxResult {
80
+ ok: boolean;
81
+ error?: string;
82
+ rollbackFailed: string[];
83
+ }
84
+ /**
85
+ * Run a sequence of filesystem operations under snapshot/rollback protection.
86
+ *
87
+ * 1. Snapshot `pathsToWatch` (current content or absent marker).
88
+ * 2. Execute each `op` in order. On the first throw, stop.
89
+ * 3. On any failure: restore all watched paths to their snapshotted state.
90
+ * Returns `{ ok: false, error, rollbackFailed }` — `rollbackFailed` lists
91
+ * paths whose restoration itself failed (partial rollback, reported honestly).
92
+ * 4. On full success: returns `{ ok: true, rollbackFailed: [] }`.
93
+ *
94
+ * Exported for direct testing; also used by all production commit/delete paths.
95
+ */
96
+ export declare function runWithRollback(pathsToWatch: string[], ops: Array<() => void>): TxResult;
97
+ export type PlanAction = {
98
+ action: 'create';
99
+ filename: string;
100
+ } | {
101
+ action: 'update';
102
+ filename: string;
103
+ } | {
104
+ action: 'refuse';
105
+ filename: string;
106
+ reason: string;
107
+ } | {
108
+ action: 'remove';
109
+ filename: string;
110
+ } | {
111
+ action: 'skip';
112
+ filename: string;
113
+ };
114
+ export interface ExportPlan {
115
+ status: 'ok' | 'error';
116
+ error?: string;
117
+ agentsRoot: string;
118
+ actions: PlanAction[];
119
+ }
120
+ export declare function preview(agentsRoot: string, configOptions?: ExportConfigOptions): ExportPlan;
121
+ export interface ExportResult {
122
+ status: 'ok' | 'error';
123
+ written: number;
124
+ skipped: number;
125
+ refused: Array<{
126
+ filename: string;
127
+ reason: string;
128
+ }>;
129
+ error?: string;
130
+ }
131
+ export declare function exportPersonas(agentsRoot: string, configOptions?: ExportConfigOptions): ExportResult;
132
+ export interface RefreshResult {
133
+ status: 'ok' | 'error';
134
+ updated: number;
135
+ skippedUnowned: number;
136
+ error?: string;
137
+ }
138
+ export declare function refresh(agentsRoot: string, configOptions?: ExportConfigOptions): RefreshResult;
139
+ export interface CleanupResult {
140
+ status: 'ok' | 'error';
141
+ error?: string;
142
+ }
143
+ export declare function cleanup(agentsRoot: string, configOptions?: ExportConfigOptions): CleanupResult;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Pure persona generation logic for pi-subagents interop.
3
+ *
4
+ * Contains the curated persona list, compatibility screening, content
5
+ * generation, and generateAll(). Importable from both src/ and scripts/.
6
+ * No filesystem writes; no CLI entrypoint.
7
+ */
8
+ /** Compatibility severity: info = fully usable, warning = may differ, critical = excluded. */
9
+ export type CompatibilitySeverity = 'info' | 'warning' | 'critical';
10
+ /** Result of classifying a persona's pi-subagents compatibility. */
11
+ export interface CompatibilityStatus {
12
+ severity: CompatibilitySeverity;
13
+ /** Human-readable reasons for the severity (empty for info). */
14
+ reasons: string[];
15
+ }
16
+ /** Per-persona entry in the manifest. */
17
+ export interface ManifestEntry {
18
+ /** Emitted filename, e.g. `systematic-best-practices-researcher.md`. */
19
+ filename: string;
20
+ /** Export status. */
21
+ status: 'exported' | 'exported-with-warning' | 'excluded-critical';
22
+ /** Repo-relative source path, e.g. `agents/research/best-practices-researcher.md`. */
23
+ sourceRelPath: string;
24
+ /** SHA-256 hex of the generated content (only for exported entries). */
25
+ hash: string;
26
+ /** Generated content (only for exported entries). */
27
+ content?: string;
28
+ /** Human-readable reason (for excluded-critical and exported-with-warning). */
29
+ reason?: string;
30
+ }
31
+ /**
32
+ * The authoritative curated-include list with per-persona compatibility
33
+ * rationale. Only personas in this list are candidates for export.
34
+ *
35
+ * Exclusion rationale (not in list):
36
+ * - agents/workflow/systematic-implementer.md — CRITICAL: dispatched-by-parent assumption.
37
+ * - agents/design/design-iterator.md — CRITICAL: requires agent-browser + skill load.
38
+ * - agents/review/agent-native-reviewer.md — CRITICAL: Systematic/OpenCode-specific context.
39
+ * - agents/review/project-standards-reviewer.md — CRITICAL: requires orchestrator <standards-paths>.
40
+ * - agents/review/kieran-typescript-reviewer.md — WARNING: excluded by plan recommendation.
41
+ * - agents/research/slack-researcher.md — CRITICAL: requires Slack MCP environment.
42
+ * - agents/research/learnings-researcher.md — CRITICAL: references Systematic skill paths.
43
+ * - agents/workflow/pr-comment-resolver.md — WARNING: "Spawned by the resolve-pr-feedback skill".
44
+ */
45
+ export interface CuratedPersonaEntry {
46
+ relPath: string;
47
+ rationale: string;
48
+ }
49
+ export declare const CURATED_PERSONAS: CuratedPersonaEntry[];
50
+ /**
51
+ * Sanitize a persona name for use as a pi-subagents filename stem.
52
+ * Returns empty string if no safe characters remain — callers must reject
53
+ * empty to avoid producing `systematic-.md`.
54
+ */
55
+ export declare function sanitizeName(name: string): string;
56
+ export declare function classifyCompatibility(content: string): CompatibilityStatus;
57
+ export declare function generatePersonaContent(sourceRef: string, rawContent: string): string | null;
58
+ export declare function generatePersonaManifest(sourceRelPath: string, rawContent: string, _repoRoot: string): ManifestEntry;
59
+ /**
60
+ * Generate all curated personas from repoRoot/agents/.
61
+ * Throws on collision, new critical coupling, or read errors.
62
+ * Pure — no writes.
63
+ */
64
+ export declare function generateAll(repoRoot: string): ManifestEntry[];