@fro.bot/systematic 3.16.2 → 3.16.4

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/dist/cli.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { type CapabilityClock, type CapabilityOutputSink, type ConfigObservationMetadata } from './lib/capability-snapshot.js';
3
- interface CapabilityCliRoots {
3
+ export interface CapabilityCliRoots {
4
4
  readonly agentsRoot: string;
5
5
  readonly cwd: string;
6
6
  readonly homeDir: string;
package/dist/cli.js CHANGED
@@ -2085,6 +2085,23 @@ function isLegacyReviewArtifact(value) {
2085
2085
  function hasParentDirectoryTraversal(input) {
2086
2086
  return input.split(/[\\/]+/).some((segment) => segment === "..");
2087
2087
  }
2088
+ var ALLOW_OUTSIDE_ARTIFACT_ROOT_FLAG = "--allow-outside-artifact-root";
2089
+ var VALIDATE_REVIEW_ARTIFACT_USAGE = "Usage: systematic validate-review-artifact <path> [--allow-outside-artifact-root]";
2090
+ function parseValidateReviewArtifactArguments(argv) {
2091
+ const allowOutsideArtifactRoot = argv.includes(ALLOW_OUTSIDE_ARTIFACT_ROOT_FLAG);
2092
+ const positional = argv.filter((arg) => arg !== ALLOW_OUTSIDE_ARTIFACT_ROOT_FLAG);
2093
+ if (positional.length !== 1)
2094
+ return;
2095
+ const path = positional[0];
2096
+ if (path === undefined)
2097
+ return;
2098
+ return { allowOutsideArtifactRoot, path };
2099
+ }
2100
+ var REVIEW_ARTIFACT_VALID_MESSAGE = "Review artifact is valid";
2101
+ var REVIEW_ARTIFACT_VALID_EXTERNAL_MESSAGE = "Review artifact is valid (validated outside the run directory; not evidence for this run)";
2102
+ function formatReviewArtifactSuccessMessage(allowOutsideArtifactRoot) {
2103
+ return allowOutsideArtifactRoot ? REVIEW_ARTIFACT_VALID_EXTERNAL_MESSAGE : REVIEW_ARTIFACT_VALID_MESSAGE;
2104
+ }
2088
2105
  function pathContainsSymlink(candidate) {
2089
2106
  let current = path3.parse(candidate).root;
2090
2107
  const relative = path3.relative(current, candidate);
@@ -2105,30 +2122,33 @@ function isWithinDirectory(candidate, directory) {
2105
2122
  const relative = path3.relative(directory, candidate);
2106
2123
  return relative !== "" && relative !== ".." && !relative.startsWith(`..${path3.sep}`) && !path3.isAbsolute(relative);
2107
2124
  }
2108
- function resolveReviewArtifactPath(input, cwd) {
2125
+ function resolveReviewArtifactPath(input, cwd, options = {}) {
2126
+ const allowOutsideArtifactRoot = options.allowOutsideArtifactRoot ?? false;
2109
2127
  if (hasParentDirectoryTraversal(input)) {
2110
2128
  return {
2111
2129
  message: "Review artifact path must not contain parent-directory traversal",
2112
2130
  ok: false
2113
2131
  };
2114
2132
  }
2115
- const artifactRoot = path3.resolve(cwd, ".context", "systematic", "ce-review");
2116
2133
  let canonicalRoot;
2117
- try {
2118
- canonicalRoot = fs4.realpathSync(artifactRoot);
2119
- if (!fs4.statSync(canonicalRoot).isDirectory()) {
2120
- return {
2121
- message: "Review artifact directory is not a directory",
2122
- ok: false
2123
- };
2134
+ if (!allowOutsideArtifactRoot) {
2135
+ const artifactRoot = path3.resolve(cwd, ".context", "systematic", "ce-review");
2136
+ try {
2137
+ canonicalRoot = fs4.realpathSync(artifactRoot);
2138
+ if (!fs4.statSync(canonicalRoot).isDirectory()) {
2139
+ return {
2140
+ message: "Review artifact directory is not a directory",
2141
+ ok: false
2142
+ };
2143
+ }
2144
+ } catch {
2145
+ return { message: "Review artifact directory is unavailable", ok: false };
2124
2146
  }
2125
- } catch {
2126
- return { message: "Review artifact directory is unavailable", ok: false };
2127
2147
  }
2128
2148
  const candidate = path3.resolve(cwd, input);
2129
2149
  if (pathContainsSymlink(candidate)) {
2130
2150
  return {
2131
- message: "Review artifact path must not contain symlinks",
2151
+ message: "Review artifact path must not contain symlinks; pass a fully resolved path",
2132
2152
  ok: false
2133
2153
  };
2134
2154
  }
@@ -2138,7 +2158,7 @@ function resolveReviewArtifactPath(input, cwd) {
2138
2158
  } catch {
2139
2159
  return { message: "Review artifact file was not found", ok: false };
2140
2160
  }
2141
- if (!isWithinDirectory(canonicalTarget, canonicalRoot)) {
2161
+ if (canonicalRoot !== undefined && !isWithinDirectory(canonicalTarget, canonicalRoot)) {
2142
2162
  return {
2143
2163
  message: "Review artifact path must remain inside .context/systematic/ce-review",
2144
2164
  ok: false
@@ -2721,9 +2741,12 @@ Usage:
2721
2741
  Commands:
2722
2742
  list [type] List available skills, agents, or commands
2723
2743
  capabilities Read-only standalone-CLI observation; not a host-runtime or canonical-registry view
2724
- validate-review-artifact <path>
2744
+ validate-review-artifact <path> [--allow-outside-artifact-root]
2725
2745
  Validate a ce:review run artifact
2726
2746
  The <path> argument is required by design; no artifact discovery is performed.
2747
+ --allow-outside-artifact-root skips the requirement that <path> resolve
2748
+ inside .context/systematic/ce-review, for validating artifacts copied from
2749
+ CI, issues, or fixtures. Not for the ce:review parent's own run artifact.
2727
2750
  Exit statuses: 0 valid artifact, 1 validation failure,
2728
2751
  2 operational failure, 3 legacy artifact with no schema_version
2729
2752
  config [subcommand] Configuration management
@@ -2744,6 +2767,7 @@ Examples:
2744
2767
  systematic list skills
2745
2768
  systematic capabilities
2746
2769
  systematic validate-review-artifact .context/systematic/ce-review/review-summary.json
2770
+ systematic validate-review-artifact --allow-outside-artifact-root /path/to/external-artifact.json
2747
2771
  systematic list agents
2748
2772
  systematic config show
2749
2773
  systematic config show --json
@@ -2774,7 +2798,6 @@ Scope:
2774
2798
  project <cwd>/.pi/agents (default)
2775
2799
  global $PI_CODING_AGENT_DIR/agents or ~/.pi/agent/agents
2776
2800
  `;
2777
- var VALIDATE_REVIEW_ARTIFACT_USAGE = "Usage: systematic validate-review-artifact <path>";
2778
2801
  var REVIEW_ARTIFACT_SCHEMA_RELATIVE_PATH = "skills/ce-review/references/review-summary-schema.json";
2779
2802
  function defaultCapabilityRoots() {
2780
2803
  const configDir = process.env.XDG_CONFIG_HOME ? path5.join(process.env.XDG_CONFIG_HOME, "opencode") : path5.join(os2.homedir(), ".config/opencode");
@@ -2935,19 +2958,17 @@ function validateReviewArtifactArgument(argv) {
2935
2958
  const commandIndex = argv[0] === "systematic" ? 1 : 0;
2936
2959
  if (argv[commandIndex] !== "validate-review-artifact")
2937
2960
  return;
2938
- if (argv.length !== commandIndex + 2)
2939
- return;
2940
- return argv[commandIndex + 1];
2961
+ return parseValidateReviewArtifactArguments(argv.slice(commandIndex + 1));
2941
2962
  }
2942
2963
  function runValidateReviewArtifact(options) {
2943
2964
  const outputSink = options.outputSink ?? ((message) => console.log(message));
2944
2965
  const errorSink = options.errorSink ?? ((message) => console.error(message));
2945
- const input = validateReviewArtifactArgument(options.argv);
2946
- if (input === undefined) {
2966
+ const parsedArgs = validateReviewArtifactArgument(options.argv);
2967
+ if (parsedArgs === undefined) {
2947
2968
  errorSink(VALIDATE_REVIEW_ARTIFACT_USAGE);
2948
2969
  return 2;
2949
2970
  }
2950
- const resolved = resolveReviewArtifactPath(input, options.cwd ?? process.cwd());
2971
+ const resolved = resolveReviewArtifactPath(parsedArgs.path, options.cwd ?? process.cwd(), { allowOutsideArtifactRoot: parsedArgs.allowOutsideArtifactRoot });
2951
2972
  if (!resolved.ok) {
2952
2973
  errorSink(resolved.message);
2953
2974
  return 2;
@@ -2972,7 +2993,7 @@ function runValidateReviewArtifact(options) {
2972
2993
  errorSink(`Review artifact validation failed: ${result.error.issues.length} issue(s)`);
2973
2994
  return 1;
2974
2995
  }
2975
- outputSink("Review artifact is valid");
2996
+ outputSink(formatReviewArtifactSuccessMessage(parsedArgs.allowOutsideArtifactRoot));
2976
2997
  return 0;
2977
2998
  }
2978
2999
  function runValidateReviewArtifactCli(options) {
package/dist/index.js CHANGED
@@ -11803,7 +11803,7 @@ function createSessionRuntime(options, operationObservers, recoveredOperationCon
11803
11803
  result.satisfiedCount = source.debug.satisfiedCount;
11804
11804
  result.missingCount = source.debug.missingCount;
11805
11805
  result.family = source.debug.family;
11806
- result.status = source.debug.status;
11806
+ result.debugEpochStatus = source.debug.status;
11807
11807
  }
11808
11808
  return result;
11809
11809
  }
@@ -11845,10 +11845,12 @@ function createSessionRuntime(options, operationObservers, recoveredOperationCon
11845
11845
  function writeCompletionResult(output, result, target) {
11846
11846
  if (!isRecord7(output))
11847
11847
  return;
11848
+ const existingMetadata = isRecord7(output.metadata) ? output.metadata : {};
11848
11849
  if (result.status === "completed" || result.status === "duplicate") {
11849
11850
  output.title = "Workflow transition completed";
11850
11851
  output.output = `workflow guard completed ${target}`;
11851
11852
  output.metadata = {
11853
+ ...existingMetadata,
11852
11854
  ...metadata2(),
11853
11855
  workflowGuard: { status: "completed", target }
11854
11856
  };
@@ -11862,6 +11864,7 @@ function createSessionRuntime(options, operationObservers, recoveredOperationCon
11862
11864
  reasonCode
11863
11865
  });
11864
11866
  output.metadata = {
11867
+ ...existingMetadata,
11865
11868
  ...metadata2(),
11866
11869
  workflowGuard: {
11867
11870
  status: resultStatus,
@@ -11870,6 +11873,19 @@ function createSessionRuntime(options, operationObservers, recoveredOperationCon
11870
11873
  }
11871
11874
  };
11872
11875
  }
11876
+ function writeAbandonedCompletionResult(output, target, reasonCode) {
11877
+ writeCompletionResult(output, { target, status: "unavailable", reasonCode }, target);
11878
+ }
11879
+ function writeAbandonedCompletionMetadata(output, target, reasonCode) {
11880
+ if (!isRecord7(output))
11881
+ return;
11882
+ const existingMetadata = isRecord7(output.metadata) ? output.metadata : {};
11883
+ output.metadata = {
11884
+ ...existingMetadata,
11885
+ ...metadata2(),
11886
+ workflowGuard: { status: "unavailable", target, reasonCode }
11887
+ };
11888
+ }
11873
11889
  function prepareSkill(host, args2) {
11874
11890
  const skill = normalizeSkill(host.tool, args2);
11875
11891
  if (!skill)
@@ -12286,6 +12302,7 @@ function createSessionRuntime(options, operationObservers, recoveredOperationCon
12286
12302
  callId: pending.callID,
12287
12303
  transitionId: pending.transitionId
12288
12304
  });
12305
+ writeAbandonedCompletionMetadata(output, pending.target, "failed-operation");
12289
12306
  return;
12290
12307
  }
12291
12308
  const result = guard.finalizeTransition({
@@ -12440,19 +12457,32 @@ function createSessionRuntime(options, operationObservers, recoveredOperationCon
12440
12457
  }
12441
12458
  return readbacks;
12442
12459
  }
12460
+ function finishMismatchedTarget(callDigest, pending, output) {
12461
+ abandonComplete(callDigest, pending, false);
12462
+ markUnavailable();
12463
+ writeAbandonedCompletionResult(output, pending.target, "invalid-transition");
12464
+ }
12465
+ function finishUnreplayableComplete(finalTarget, output) {
12466
+ markUnavailable();
12467
+ writeAbandonedCompletionResult(output, finalTarget ?? "unit", "guard-unavailable");
12468
+ }
12469
+ function finishUnavailableReadback(callDigest, pending, output) {
12470
+ abandonComplete(callDigest, pending, true);
12471
+ markUnavailable();
12472
+ writeAbandonedCompletionResult(output, pending.target, "finalization-failed");
12473
+ }
12443
12474
  async function finishComplete(host, output) {
12444
12475
  const callDigest = digestCall(ledger, host.callID);
12445
12476
  const pending = pendingCompletes.get(callDigest);
12446
12477
  const finalTarget = normalizeTarget(host.args);
12447
12478
  if (pending && (!finalTarget || finalTarget !== pending.target)) {
12448
- abandonComplete(callDigest, pending, false);
12449
- markUnavailable();
12479
+ finishMismatchedTarget(callDigest, pending, output);
12450
12480
  return;
12451
12481
  }
12452
12482
  if (!pending) {
12453
12483
  if (replayTerminalComplete(callDigest, finalTarget, output))
12454
12484
  return;
12455
- markUnavailable();
12485
+ finishUnreplayableComplete(finalTarget, output);
12456
12486
  return;
12457
12487
  }
12458
12488
  if (!options.observer) {
@@ -12461,8 +12491,7 @@ function createSessionRuntime(options, operationObservers, recoveredOperationCon
12461
12491
  }
12462
12492
  const readbacks = await completionReadbacks();
12463
12493
  if (readbacks.status === "unavailable") {
12464
- abandonComplete(callDigest, pending, true);
12465
- markUnavailable();
12494
+ finishUnavailableReadback(callDigest, pending, output);
12466
12495
  return;
12467
12496
  }
12468
12497
  finalizeComplete(callDigest, pending, output, readbacks.status === "ready" ? readbacks.readbacks : undefined);
@@ -1,4 +1,6 @@
1
1
  import type { Config } from '@opencode-ai/plugin';
2
+ import type { AgentConfig } from '@opencode-ai/sdk';
3
+ import { type PermissionSetting } from './validation.js';
2
4
  export interface ConfigHandlerDeps {
3
5
  directory: string;
4
6
  bundledSkillsDir: string;
@@ -11,6 +13,41 @@ export interface ConfigHandlerDeps {
11
13
  }
12
14
  export declare function toTitleCase(name: string): string;
13
15
  export declare function formatAgentDescription(name: string, description: string | undefined): string;
16
+ /**
17
+ * Per-tool permission rule map as actually emitted by {@link permissionFromRules}:
18
+ * each key is a match pattern (`'*'` or an exact name) mapped to a setting.
19
+ */
20
+ export type AgentPermissionRuleMap = Record<string, PermissionSetting>;
21
+ /**
22
+ * Agent permission shape as emitted onto `AgentConfig.permission`, narrowed at
23
+ * this module's boundary to include `skill`. The `@opencode-ai/sdk` v1 `Config`
24
+ * type this module targets omits `skill` from `AgentConfig['permission']`
25
+ * (it only appears on the SDK's v2 `PermissionRuleConfig` surface), but
26
+ * `applyPermissionOverlay`/`addManagedSkillRules` genuinely write a `skill`
27
+ * rule map identical in shape to `bash`. This type documents and reads that
28
+ * real runtime shape without migrating the module to the v2 type surface.
29
+ *
30
+ * `permissionFromRules` emits a rule map for whatever tool key gets passed to
31
+ * `setPermissionRule` — not just the named keys below (e.g. `task`, per
32
+ * `PermissionConfig` in validation.ts and the `task` overlay field applied at
33
+ * config-handler.ts's overlay pass). The named keys stay for documentation of
34
+ * the common cases; the index signature covers every other emitted tool key.
35
+ */
36
+ export interface EmittedAgentPermission {
37
+ edit?: AgentPermissionRuleMap;
38
+ bash?: AgentPermissionRuleMap;
39
+ webfetch?: AgentPermissionRuleMap;
40
+ doom_loop?: AgentPermissionRuleMap;
41
+ external_directory?: AgentPermissionRuleMap;
42
+ skill?: AgentPermissionRuleMap;
43
+ readonly [tool: string]: AgentPermissionRuleMap | undefined;
44
+ }
45
+ /**
46
+ * Read an agent's emitted permission map, narrowed to include `skill` (see
47
+ * {@link EmittedAgentPermission}). Use this instead of `AgentConfig['permission']`
48
+ * when a caller needs to observe the `skill` rule map this module writes.
49
+ */
50
+ export declare function readEmittedAgentPermission(agent: AgentConfig | undefined): EmittedAgentPermission | undefined;
14
51
  /**
15
52
  * Create the config hook handler for the Systematic plugin.
16
53
  *
@@ -16,5 +16,16 @@ export declare function formatReviewArtifactIssuePath(issuePath: readonly Proper
16
16
  export declare function readReviewArtifact(filePath: string): ReadArtifactResult;
17
17
  export declare function isLegacyReviewArtifact(value: unknown): boolean;
18
18
  export declare function hasParentDirectoryTraversal(input: string): boolean;
19
+ export declare const ALLOW_OUTSIDE_ARTIFACT_ROOT_FLAG = "--allow-outside-artifact-root";
20
+ export declare const VALIDATE_REVIEW_ARTIFACT_USAGE = "Usage: systematic validate-review-artifact <path> [--allow-outside-artifact-root]";
21
+ export interface ValidateReviewArtifactArguments {
22
+ readonly path: string;
23
+ readonly allowOutsideArtifactRoot: boolean;
24
+ }
25
+ export declare function parseValidateReviewArtifactArguments(argv: readonly string[]): ValidateReviewArtifactArguments | undefined;
26
+ export declare function formatReviewArtifactSuccessMessage(allowOutsideArtifactRoot: boolean): string;
19
27
  export declare function pathContainsSymlink(candidate: string): boolean;
20
- export declare function resolveReviewArtifactPath(input: string, cwd: string): ArtifactPathResult;
28
+ export interface ResolveReviewArtifactPathOptions {
29
+ readonly allowOutsideArtifactRoot?: boolean;
30
+ }
31
+ export declare function resolveReviewArtifactPath(input: string, cwd: string, options?: ResolveReviewArtifactPathOptions): ArtifactPathResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "3.16.2",
3
+ "version": "3.16.4",
4
4
  "description": "Compound-engineering loops for OpenCode, Pi, and Claude Code",
5
5
  "type": "module",
6
6
  "homepage": "https://fro.bot/systematic",
@@ -40,6 +40,8 @@
40
40
  "test:integration": "bun test tests/integration",
41
41
  "test:all": "bun test",
42
42
  "typecheck": "tsc --noEmit",
43
+ "typecheck:scripts": "tsc -p tsconfig.scripts.json",
44
+ "typecheck:all": "tsc -p tsconfig.tests.json",
43
45
  "lint": "biome check .",
44
46
  "fix": "bun run lint --fix",
45
47
  "docs:dev": "bun run --cwd docs dev",
@@ -93,10 +95,10 @@
93
95
  }
94
96
  },
95
97
  "devDependencies": {
96
- "@biomejs/biome": "2.5.11",
98
+ "@biomejs/biome": "2.5.12",
97
99
  "@earendil-works/pi-coding-agent": "0.83.0",
98
- "@opencode-ai/plugin": "1.18.21",
99
- "@opencode-ai/sdk": "1.18.21",
100
+ "@opencode-ai/plugin": "1.18.27",
101
+ "@opencode-ai/sdk": "1.18.27",
100
102
  "@semantic-release/exec": "7.1.0",
101
103
  "@tintinweb/pi-subagents": "0.14.3",
102
104
  "@types/bun": "latest",
@@ -247,6 +247,18 @@ validation, and it does not delete the artifact to escape the check. A failing
247
247
  artifact is evidence and stays on disk; an absent artifact is never evidence
248
248
  of a clean run.
249
249
 
250
+ Both commands also accept `--allow-outside-artifact-root`, which skips the
251
+ containment check so a copy fetched from CI, an issue attachment, or a fixture
252
+ can be validated from outside `.context/systematic/ce-review`. It also drops
253
+ the requirement that `.context/systematic/ce-review` exist at all. That is not
254
+ a workaround for the "directory is unavailable" degradation described above:
255
+ that degradation is deliberate visible failure for the parent's own run
256
+ artifact, and the flag does not change how the parent's own validation run is
257
+ judged. This is an external-validation mode for artifacts the parent did not
258
+ just produce. The parent must never pass this flag when validating its own
259
+ run's `review-summary.json`; doing so would silently accept a mismatched or
260
+ misplaced artifact as this run's evidence.
261
+
250
262
  This is enforcement by visible failure, not by containment. An agent that
251
263
  never runs the command can still finalize an artifact, but produces no evidence
252
264
  in either direction. That is why the command exists as an independently