@fro.bot/systematic 3.4.1 → 3.5.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.
@@ -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[];
@@ -1,5 +1,6 @@
1
1
  import { type ReceiptClassifier } from './receipt-classifier.js';
2
2
  import type { ReceiptLedger, ReceiptOperation } from './receipt-ledger.js';
3
+ import type { ReceiptResourceScope } from './receipt-readback.js';
3
4
  export type WorkflowMode = 'protected' | 'disabled' | 'unavailable';
4
5
  export type WorkflowState = 'protected' | 'waiting' | 'rejected' | 'disabled' | 'unavailable';
5
6
  export type RepairKind = 'fresh-readback' | 'rerun-operation' | 'question-attestation';
@@ -34,6 +35,12 @@ export interface UnitSnapshot {
34
35
  status: 'active' | 'completed';
35
36
  requiredOperations: readonly ReceiptOperation[];
36
37
  requiredResourceOperations: readonly ReceiptOperation[];
38
+ resourceScopes: readonly ReceiptResourceScope[];
39
+ }
40
+ export interface CurrentOperationContext {
41
+ readonly workspaceIdentity: string;
42
+ readonly repositoryIdentity?: string;
43
+ readonly worktreeIdentity?: string;
37
44
  }
38
45
  export interface WorkflowStatus {
39
46
  state: WorkflowState;
@@ -125,7 +132,14 @@ export interface WorkflowGuard {
125
132
  observeReceipt(input: unknown): EvidenceObservationResult;
126
133
  observeAttempt(input: unknown): EvidenceObservationResult;
127
134
  observeOperation(input: unknown): Promise<EvidenceObservationResult>;
135
+ /**
136
+ * Internal recovery seam. Callers MUST validate host lineage, own
137
+ * registration/seed/readback, stable workspace, and current mutable
138
+ * revisions before using this classifier-bypassing path.
139
+ */
140
+ observeTrustedRecoveredOperation(input: unknown): Promise<EvidenceObservationResult>;
128
141
  observeReadback(input: unknown): ReadbackObservationResult;
142
+ currentOperationContext(): CurrentOperationContext;
129
143
  status(): WorkflowStatus;
130
144
  prepareTransition(input: unknown): TransitionPrepareResult;
131
145
  finalizeTransition(input: unknown): TransitionFinalizeResult;
@@ -26,8 +26,11 @@
26
26
  "workflow_guard": {
27
27
  "$ref": "#/definitions/__schema57"
28
28
  },
29
- "skills_as_commands": {
29
+ "pi_subagents": {
30
30
  "$ref": "#/definitions/__schema61"
31
+ },
32
+ "skills_as_commands": {
33
+ "$ref": "#/definitions/__schema72"
31
34
  }
32
35
  },
33
36
  "additionalProperties": false,
@@ -1368,16 +1371,178 @@
1368
1371
  "type": "boolean"
1369
1372
  },
1370
1373
  "__schema61": {
1374
+ "default": {
1375
+ "categories": {},
1376
+ "agents": {}
1377
+ },
1378
+ "description": "Pi-native pi-subagents export field overlays (thinking, max_turns, tools, skills). Category values apply first; per-agent values override. No model field — model stays in the categories/agents overlay.",
1379
+ "examples": [
1380
+ {
1381
+ "categories": {
1382
+ "research": {
1383
+ "thinking": "high"
1384
+ }
1385
+ },
1386
+ "agents": {
1387
+ "repo-research-analyst": {
1388
+ "max_turns": 10
1389
+ }
1390
+ }
1391
+ }
1392
+ ],
1393
+ "allOf": [
1394
+ {
1395
+ "$ref": "#/definitions/__schema62"
1396
+ }
1397
+ ]
1398
+ },
1399
+ "__schema62": {
1400
+ "type": "object",
1401
+ "properties": {
1402
+ "categories": {
1403
+ "default": {},
1404
+ "description": "Per-category pi-subagents export overlays keyed by category name",
1405
+ "examples": [
1406
+ {
1407
+ "research": {
1408
+ "thinking": "high"
1409
+ }
1410
+ },
1411
+ {}
1412
+ ],
1413
+ "allOf": [
1414
+ {
1415
+ "$ref": "#/definitions/__schema63"
1416
+ }
1417
+ ]
1418
+ },
1419
+ "agents": {
1420
+ "default": {},
1421
+ "description": "Per-agent pi-subagents export overlays keyed by bundled agent name",
1422
+ "examples": [
1423
+ {
1424
+ "repo-research-analyst": {
1425
+ "max_turns": 10
1426
+ }
1427
+ },
1428
+ {}
1429
+ ],
1430
+ "allOf": [
1431
+ {
1432
+ "$ref": "#/definitions/__schema71"
1433
+ }
1434
+ ]
1435
+ }
1436
+ },
1437
+ "additionalProperties": false
1438
+ },
1439
+ "__schema63": {
1440
+ "type": "object",
1441
+ "propertyNames": {
1442
+ "type": "string"
1443
+ },
1444
+ "additionalProperties": {
1445
+ "description": "Per-category pi-subagents export overlay (Pi-native fields only; no model)",
1446
+ "examples": [
1447
+ {
1448
+ "thinking": "medium"
1449
+ }
1450
+ ],
1451
+ "allOf": [
1452
+ {
1453
+ "$ref": "#/definitions/__schema70"
1454
+ }
1455
+ ]
1456
+ }
1457
+ },
1458
+ "__schema64": {
1459
+ "type": "string",
1460
+ "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max"],
1461
+ "description": "pi-subagents reasoning effort level for exported persona frontmatter",
1462
+ "examples": ["off", "medium", "high"],
1463
+ "trust": "project-or-higher"
1464
+ },
1465
+ "__schema65": {
1466
+ "type": "integer",
1467
+ "minimum": 0,
1468
+ "maximum": 9007199254740991,
1469
+ "description": "pi-subagents maximum turns for a delegated persona (0 = unlimited)",
1470
+ "examples": [0, 10, 25],
1471
+ "trust": "any"
1472
+ },
1473
+ "__schema66": {
1474
+ "type": "string",
1475
+ "minLength": 1,
1476
+ "description": "pi-subagents comma-selector tool string (built-ins, */all/none, or extension selectors)",
1477
+ "examples": ["*", "read,grep,glob", "none"],
1478
+ "trust": "project-or-higher"
1479
+ },
1480
+ "__schema67": {
1481
+ "anyOf": [
1482
+ {
1483
+ "$ref": "#/definitions/__schema68"
1484
+ },
1485
+ {
1486
+ "$ref": "#/definitions/__schema69"
1487
+ }
1488
+ ],
1489
+ "description": "pi-subagents skills selector: true (all) or a comma-separated list of skill names",
1490
+ "examples": [true, "ce:plan,ce:review"],
1491
+ "trust": "project-or-higher"
1492
+ },
1493
+ "__schema68": {
1494
+ "type": "boolean",
1495
+ "const": true
1496
+ },
1497
+ "__schema69": {
1498
+ "type": "string",
1499
+ "minLength": 1
1500
+ },
1501
+ "__schema70": {
1502
+ "type": "object",
1503
+ "properties": {
1504
+ "thinking": {
1505
+ "$ref": "#/definitions/__schema64"
1506
+ },
1507
+ "max_turns": {
1508
+ "$ref": "#/definitions/__schema65"
1509
+ },
1510
+ "tools": {
1511
+ "$ref": "#/definitions/__schema66"
1512
+ },
1513
+ "skills": {
1514
+ "$ref": "#/definitions/__schema67"
1515
+ }
1516
+ },
1517
+ "additionalProperties": false,
1518
+ "description": "Per-agent pi-subagents export overlay (Pi-native fields only; no model)",
1519
+ "examples": [
1520
+ {
1521
+ "thinking": "high",
1522
+ "max_turns": 10
1523
+ }
1524
+ ]
1525
+ },
1526
+ "__schema71": {
1527
+ "type": "object",
1528
+ "propertyNames": {
1529
+ "type": "string"
1530
+ },
1531
+ "additionalProperties": {
1532
+ "$ref": "#/definitions/__schema70"
1533
+ }
1534
+ },
1535
+ "__schema72": {
1371
1536
  "default": true,
1372
1537
  "description": "Register skills discovered from user/project skill directories (OpenCode config and other agent-harness-standard locations) as slash commands. Default true.",
1373
1538
  "examples": [true, false],
1374
1539
  "allOf": [
1375
1540
  {
1376
- "$ref": "#/definitions/__schema62"
1541
+ "$ref": "#/definitions/__schema73"
1377
1542
  }
1378
1543
  ]
1379
1544
  },
1380
- "__schema62": {
1545
+ "__schema73": {
1381
1546
  "type": "boolean"
1382
1547
  }
1383
1548
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "3.4.1",
3
+ "version": "3.5.1",
4
4
  "description": "Compound-engineering loops for OpenCode, Pi, and Claude Code",
5
5
  "type": "module",
6
6
  "homepage": "https://fro.bot/systematic",
@@ -90,11 +90,12 @@
90
90
  }
91
91
  },
92
92
  "devDependencies": {
93
- "@biomejs/biome": "2.5.5",
93
+ "@biomejs/biome": "2.5.6",
94
94
  "@earendil-works/pi-coding-agent": "0.80.10",
95
- "@opencode-ai/plugin": "1.18.5",
96
- "@opencode-ai/sdk": "1.18.5",
95
+ "@opencode-ai/plugin": "1.18.8",
96
+ "@opencode-ai/sdk": "1.18.8",
97
97
  "@semantic-release/exec": "7.1.0",
98
+ "@tintinweb/pi-subagents": "0.14.3",
98
99
  "@types/bun": "latest",
99
100
  "@types/js-yaml": "4.0.9",
100
101
  "@types/node": "24.13.3",
@@ -4,14 +4,14 @@ Evidence registry: see [`HARNESSES.md`](../../../HARNESSES.md).
4
4
 
5
5
  | Capability | Mechanism | Status | Fallback |
6
6
  |---|---|---|---|
7
- | Subagent delegation | `systematic_delegate({agent, task})` with bundled personas | degraded; execution is sequential only | Dispatch sequentially in dependency order or do the work inline |
7
+ | Subagent delegation | Bounded built-in delegate via `systematic_delegate({agent, task})`; optional mature delegation via [pi-subagents](https://github.com/tintinweb/pi-subagents) through opt-in persona export | built-in: degraded (sequential only); pi-subagents path: outside Systematic's bounded-delegate guarantees | Dispatch sequentially in dependency order or do the work inline |
8
8
  | Blocking user interaction | No native blocking tool | degraded | Present numbered options in chat and wait |
9
9
  | Task tracking | No native task-tracking mechanism | unavailable | Maintain a visible task list in responses |
10
10
  | Skill loading | `systematic_skill` tool or Pi-native skill activation | supported | Read the skill instructions listed by the active harness |
11
11
 
12
12
  ## Invocation examples
13
13
 
14
- ### Subagent delegation
14
+ ### Subagent delegation (built-in)
15
15
 
16
16
  ```typescript
17
17
  systematic_delegate({
@@ -20,6 +20,32 @@ systematic_delegate({
20
20
  })
21
21
  ```
22
22
 
23
+ `systematic_delegate` is sequential, capped at 20 turns, depth-1, and spawns its child with `noExtensions: true`. The `noExtensions` guarantee bounds `systematic_delegate`'s own recursion — it does not bound end-to-end delegation depth across a combined pi-subagents + Systematic path.
24
+
25
+ ### Optional: pi-subagents delegation
26
+
27
+ Export Systematic personas for use with [pi-subagents](https://github.com/tintinweb/pi-subagents) (parallel/multi-model delegation). All writes are opt-in; nothing is exported at extension load.
28
+
29
+ Tested against pi-subagents v0.14.3 (verified contract as of July 29, 2026). Versions outside the tested range are unsupported but nonfatal.
30
+
31
+ Exact CLI form: `systematic pi-subagents <preview|export|refresh|cleanup> --scope project|global`. `--scope` is optional and defaults to `project`.
32
+
33
+ ```bash
34
+ systematic pi-subagents preview [--scope project|global]
35
+ systematic pi-subagents export [--scope project|global]
36
+ systematic pi-subagents refresh [--scope project|global]
37
+ systematic pi-subagents cleanup [--scope project|global]
38
+ ```
39
+
40
+ - `project` (default) targets `<cwd>/.pi/agents/`.
41
+ - `global` targets `$PI_CODING_AGENT_DIR/agents/`, or `~/.pi/agent/agents/` if `$PI_CODING_AGENT_DIR` is unset.
42
+
43
+ The pi-subagents delegation path is **outside Systematic's bounded-delegate guarantees** and governed by pi-subagents' own configuration. `systematic_delegate` remains the bounded default.
44
+
45
+ `export`/`refresh`/`cleanup` hold an exclusive per-root mutation lock and fail closed if any path component between the selected scope's anchor and the agents directory is a symlink or not a directory. A manifest (`.systematic-personas.json`) tracks ownership by filename and content hash; cleanup and stale-file removal only delete a file whose on-disk content still matches its recorded hash, refusing otherwise. Manifest hashes are drift evidence, not a cryptographic authenticity guarantee.
46
+
47
+ Systematic's own config (`systematic.json`/`.jsonc`) is the durable source of truth for exported personas; the generated files and their manifest are a disposable projection that `export`/`refresh`/`cleanup` regenerate or remove, and `refresh` intentionally overwrites manifest-owned generated files that diverge from current config — run `preview` first to see what would change. See the [pi-subagents pairing guide](https://fro.bot/systematic/guides/pi-subagents/) for the config precedence, trust boundaries, and a canonical example.
48
+
23
49
  ### Blocking user interaction
24
50
 
25
51
  ```text