@stackwright-pro/otters 1.0.0-alpha.7 → 1.0.0-alpha.71

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/package.json CHANGED
@@ -1,21 +1,23 @@
1
1
  {
2
2
  "name": "@stackwright-pro/otters",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.71",
4
4
  "description": "Stackwright Pro Otter Raft - AI agents for enterprise features (CAC auth, API dashboards, government use cases)",
5
- "license": "MIT",
5
+ "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/Per-Aspera-LLC/stackwright-pro"
9
9
  },
10
10
  "devDependencies": {
11
- "vitest": "^4.0.18",
12
- "zod": "^3.22.4"
11
+ "vitest": "^4.1.8",
12
+ "zod": "^4.4.3"
13
13
  },
14
14
  "exports": {
15
15
  "./src": "./src",
16
- "./pro-foreman": "./src/stackwright-pro-foreman-otter.json"
16
+ "./pro-foreman": "./src/stackwright-pro-foreman-otter.json",
17
+ "./pro-workflow": "./src/stackwright-pro-form-wizard-otter.json"
17
18
  },
18
19
  "files": [
20
+ "fixtures",
19
21
  "scripts",
20
22
  "src"
21
23
  ],
@@ -23,7 +25,7 @@
23
25
  "access": "public"
24
26
  },
25
27
  "peerDependencies": {
26
- "@stackwright-pro/mcp": "^0.2.0-alpha.1"
28
+ "@stackwright-pro/mcp": "^0.2.0-alpha.104"
27
29
  },
28
30
  "scripts": {
29
31
  "generate-checksums": "node scripts/generate-checksums.js",
@@ -31,6 +33,7 @@
31
33
  "postinstall": "node scripts/install-agents.js",
32
34
  "test": "vitest run",
33
35
  "test:watch": "vitest",
34
- "test:coverage": "vitest run --coverage"
36
+ "test:coverage": "vitest run --coverage",
37
+ "sanity-check-pipeline": "node scripts/sanity-check-pipeline.mjs"
35
38
  }
36
39
  }
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ add-pipeline-declarations.py
4
+ Inserts the `pipeline` I/O declaration into each specialist otter JSON.
5
+
6
+ Run once: python3 scripts/add-pipeline-declarations.py
7
+ Part of swp-amyw Batch 1.
8
+
9
+ AUDIT NOTES (inline justifications for dep changes):
10
+ - auth-otter: was ['pages', 'dashboard', 'workflow', 'geo'] in PHASE_DEPENDENCIES.
11
+ Bead 3 (swp-e4y6) changed the runtime model: AuthProvider reads _auth.json at
12
+ runtime, no longer needs build-time route enumeration. Auth only needs
13
+ design-language.json to extract personas/roles. Moves auth wave 4 → wave 2.
14
+
15
+ - workflow (form-wizard-otter): was [] with a Prism mock fallback comment.
16
+ Bead swp-4071 (services hooks contract) made the workflow→api relationship
17
+ explicit via serviceHooks referencing API endpoints. Real deps added:
18
+ _theme.json (UI generation) and api-config.json (hook endpoint references).
19
+ """
20
+
21
+ import json
22
+ import os
23
+
24
+ SRC_DIR = os.path.join(os.path.dirname(__file__), '..', 'src')
25
+
26
+ # Pipeline declarations keyed by otter filename (no .json extension).
27
+ # Matches the table in the swp-amyw plan.
28
+ # Null-sentinel otters (foreman, domain-expert) are skipped — they're orchestrators.
29
+ DECLARATIONS: dict[str, dict] = {
30
+ 'stackwright-pro-designer-otter': {
31
+ 'inputs': {},
32
+ 'outputs': {
33
+ 'artifact': 'design-language.json',
34
+ },
35
+ },
36
+ 'stackwright-pro-theme-otter': {
37
+ 'inputs': {
38
+ 'artifacts': ['design-language.json'],
39
+ },
40
+ 'outputs': {
41
+ 'sinks': ['_theme.json'],
42
+ 'artifact': 'theme-tokens.json',
43
+ 'files': ['stackwright.theme.yml'],
44
+ },
45
+ },
46
+ 'stackwright-pro-api-otter': {
47
+ 'inputs': {},
48
+ 'outputs': {
49
+ 'sinks': ['_integrations.json'],
50
+ 'artifact': 'api-config.json',
51
+ 'files': ['stackwright.integrations.yml'],
52
+ },
53
+ },
54
+ # AUDIT: auth moved from wave 4 → wave 2.
55
+ # Bead 3 (swp-e4y6) changed the runtime model: AuthProvider reads
56
+ # _auth.json at runtime; auth no longer needs build-time route enumeration.
57
+ # Auth now only reads design-language.json to extract personas/roles.
58
+ 'stackwright-pro-auth-otter': {
59
+ 'inputs': {
60
+ 'artifacts': ['design-language.json'],
61
+ },
62
+ 'outputs': {
63
+ 'sinks': ['_auth.json'],
64
+ 'artifact': 'auth-config.json',
65
+ 'files': [
66
+ 'stackwright.auth.yml',
67
+ '.env.example',
68
+ 'lib/mock-auth.ts',
69
+ 'package.json',
70
+ ],
71
+ },
72
+ },
73
+ 'stackwright-pro-scaffold-otter': {
74
+ 'inputs': {
75
+ 'sinks': ['_theme.json'],
76
+ 'files': ['stackwright.yml'],
77
+ },
78
+ 'outputs': {
79
+ 'artifact': 'scaffold-manifest.json',
80
+ 'files': ['app/**'],
81
+ },
82
+ # scaffold-otter generates app/layout.tsx which reads _collections.json
83
+ # and _auth.json at app build time — NOT at scaffold-otter execution time.
84
+ 'runtimeReads': ['_collections.json', '_auth.json'],
85
+ },
86
+ 'stackwright-pro-data-otter': {
87
+ 'inputs': {
88
+ 'artifacts': ['api-config.json'],
89
+ },
90
+ 'outputs': {
91
+ 'sinks': ['_collections.json'],
92
+ 'artifact': 'data-config.json',
93
+ 'files': ['stackwright.collections.yml'],
94
+ },
95
+ },
96
+ 'stackwright-pro-geo-otter': {
97
+ 'inputs': {
98
+ 'artifacts': ['data-config.json'],
99
+ },
100
+ 'outputs': {
101
+ 'artifact': 'geo-manifest.json',
102
+ },
103
+ },
104
+ # AUDIT: workflow moved from [] → real deps.
105
+ # swp-4071 (services hooks contract) made workflow→api relationship explicit:
106
+ # serviceHooks reference API endpoints from api-config.json.
107
+ # UI generation requires _theme.json for component selection.
108
+ 'stackwright-pro-form-wizard-otter': {
109
+ 'inputs': {
110
+ 'sinks': ['_theme.json'],
111
+ 'artifacts': ['api-config.json'],
112
+ },
113
+ 'outputs': {
114
+ 'artifact': 'workflow-config.json',
115
+ 'files': ['stackwright.workflow.yml'],
116
+ },
117
+ },
118
+ 'stackwright-services-otter': {
119
+ 'inputs': {
120
+ 'artifacts': ['api-config.json', 'workflow-config.json'],
121
+ },
122
+ 'outputs': {
123
+ 'artifact': 'services-config.json',
124
+ },
125
+ },
126
+ 'stackwright-pro-page-otter': {
127
+ 'inputs': {
128
+ 'sinks': ['_theme.json', '_collections.json', '_auth.json'],
129
+ 'artifacts': [
130
+ 'design-language.json',
131
+ 'data-config.json',
132
+ 'services-config.json',
133
+ 'geo-manifest.json',
134
+ ],
135
+ },
136
+ 'outputs': {
137
+ 'artifact': 'pages-manifest.json',
138
+ },
139
+ },
140
+ 'stackwright-pro-dashboard-otter': {
141
+ 'inputs': {
142
+ 'sinks': ['_theme.json', '_collections.json', '_auth.json'],
143
+ 'artifacts': [
144
+ 'design-language.json',
145
+ 'data-config.json',
146
+ 'services-config.json',
147
+ 'geo-manifest.json',
148
+ ],
149
+ },
150
+ 'outputs': {
151
+ 'artifact': 'dashboard-manifest.json',
152
+ },
153
+ },
154
+ 'stackwright-pro-polish-otter': {
155
+ 'inputs': {
156
+ 'artifacts': [
157
+ 'pages-manifest.json',
158
+ 'dashboard-manifest.json',
159
+ 'workflow-config.json',
160
+ 'auth-config.json',
161
+ ],
162
+ },
163
+ 'outputs': {
164
+ 'artifact': 'polish-manifest.json',
165
+ 'files': ['stackwright.yml'],
166
+ },
167
+ },
168
+ }
169
+
170
+
171
+ def add_pipeline(filename_stem: str, declaration: dict) -> None:
172
+ path = os.path.join(SRC_DIR, f'{filename_stem}.json')
173
+ if not os.path.exists(path):
174
+ print(f' SKIP (not found): {filename_stem}.json')
175
+ return
176
+
177
+ with open(path, 'r', encoding='utf-8') as fh:
178
+ data = json.load(fh)
179
+
180
+ if 'pipeline' in data:
181
+ print(f' ⏭ ALREADY HAS pipeline: {filename_stem}.json — overwriting')
182
+
183
+ data['pipeline'] = declaration
184
+
185
+ serialized = json.dumps(data, indent=2, ensure_ascii=False) + '\n'
186
+ with open(path, 'w', encoding='utf-8') as fh:
187
+ fh.write(serialized)
188
+
189
+ print(f' {filename_stem}.json')
190
+
191
+
192
+ def main() -> None:
193
+ print('Adding pipeline declarations to specialist otter manifests...\n')
194
+ for stem, decl in DECLARATIONS.items():
195
+ add_pipeline(stem, decl)
196
+
197
+ print('\nDone. Run `node scripts/generate-checksums.js` to update checksums.json.')
198
+
199
+
200
+ if __name__ == '__main__':
201
+ main()
@@ -3,7 +3,10 @@
3
3
  * generate-checksums.js
4
4
  * Computes SHA-256 for every *-otter.json in src/ and writes src/checksums.json
5
5
  * Run: node scripts/generate-checksums.js
6
- * Auto-run: npm prepare (before publish), npm pretest (before tests)
6
+ * Auto-run: Husky pre-commit hook (see .husky/pre-commit) triggers automatically
7
+ * whenever a packages/otters/src/*-otter.json file is staged for commit.
8
+ * Note: hook is local-only; GitHub PR merges bypass it. If checksums drift
9
+ * after a remote merge, run this script manually and commit the result.
7
10
  */
8
11
 
9
12
  'use strict';
@@ -35,7 +38,6 @@ async function generateChecksums() {
35
38
  const manifest = {
36
39
  version: '1.0',
37
40
  algorithm: 'sha256',
38
- generated: new Date().toISOString(),
39
41
  files,
40
42
  };
41
43
 
@@ -8,14 +8,17 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const os = require('os');
10
10
 
11
- const AGENTS_DIR = path.join(os.homedir(), '.code_puppy', 'agents');
11
+ // Allow test overrides via env vars — production paths are the default.
12
+ // STACKWRIGHT_AGENTS_DIR: override destination (normally ~/.code_puppy/agents/)
13
+ // STACKWRIGHT_OTTERS_SRC_DIR: override source (normally <package>/src/)
14
+ const AGENTS_DIR = process.env.STACKWRIGHT_AGENTS_DIR || path.join(os.homedir(), '.code_puppy', 'agents');
12
15
 
13
16
  // Resolve package root relative to this script's location
14
17
  const scriptDir = __dirname;
15
18
  const packageRoot = path.resolve(scriptDir, '..');
16
19
 
17
20
  // All Pro otters are in the src/ directory
18
- const srcDir = path.join(packageRoot, 'src');
21
+ const srcDir = process.env.STACKWRIGHT_OTTERS_SRC_DIR || path.join(packageRoot, 'src');
19
22
 
20
23
  async function installAgents() {
21
24
  try {
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ⚠️ DEPRECATED — This launcher has been replaced by @stackwright-pro/raft.
4
+ // Run: npx @stackwright-pro/raft
5
+ // This file is kept for users who may have cached a previous install.
6
+ // It will be removed in a future release.
7
+
8
+ console.error('⚠️ launch-raft in @stackwright-pro/otters is deprecated.');
9
+ console.error(' Use: npx @stackwright-pro/raft');
10
+ console.error('');
11
+ console.error(' Install: npm install -g @stackwright-pro/raft');
12
+ console.error(' Or run directly: npx @stackwright-pro/raft');
13
+ process.exit(1);
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sanity-check-pipeline.mjs
4
+ * Verifies pipeline I/O declarations across all specialist otter manifests:
5
+ * 1. Every specialist otter (except foreman + domain-expert) has a pipeline field
6
+ * 2. No dangling sink references (every inputs.sinks value exists in some otter's outputs.sinks)
7
+ * 3. No dangling artifact references (every inputs.artifacts value exists in some otter's outputs.artifact)
8
+ * 4. No cycles (topological sort succeeds)
9
+ *
10
+ * Part of swp-amyw Batch 1 quality gate.
11
+ */
12
+
13
+ import { readFileSync, readdirSync } from 'fs';
14
+ import { join, dirname } from 'path';
15
+ import { fileURLToPath } from 'url';
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const SRC_DIR = join(__dirname, '..', 'src');
19
+
20
+ // Orchestrators: exempt from pipeline requirement
21
+ const ORCHESTRATORS = new Set([
22
+ 'stackwright-pro-foreman-otter',
23
+ 'stackwright-pro-domain-expert-otter',
24
+ ]);
25
+
26
+ // ─── Load all otter manifests ────────────────────────────────────────────────
27
+
28
+ const otterFiles = readdirSync(SRC_DIR).filter(
29
+ (f) => f.endsWith('-otter.json') && !f.startsWith('checksums')
30
+ );
31
+
32
+ const otters = otterFiles.map((filename) => {
33
+ const raw = readFileSync(join(SRC_DIR, filename), 'utf-8');
34
+ const data = JSON.parse(raw);
35
+ return { filename, name: data.name, pipeline: data.pipeline ?? null };
36
+ });
37
+
38
+ let errors = 0;
39
+
40
+ // ─── Check 1: Every specialist has a pipeline field ─────────────────────────
41
+
42
+ console.log('\n[1] Pipeline field presence...');
43
+ for (const { filename, name, pipeline } of otters) {
44
+ if (ORCHESTRATORS.has(name)) {
45
+ console.log(` SKIP (orchestrator): ${filename}`);
46
+ continue;
47
+ }
48
+ if (pipeline === null || pipeline === undefined) {
49
+ console.error(` FAIL: missing pipeline in ${filename}`);
50
+ errors++;
51
+ } else {
52
+ console.log(` OK : ${filename}`);
53
+ }
54
+ }
55
+
56
+ // ─── Collect all declared sinks + artifacts ──────────────────────────────────
57
+
58
+ const allOutputSinks = new Set();
59
+ const allOutputArtifacts = new Set();
60
+
61
+ for (const { pipeline } of otters) {
62
+ if (!pipeline) continue;
63
+ (pipeline.outputs?.sinks ?? []).forEach((s) => allOutputSinks.add(s));
64
+ if (pipeline.outputs?.artifact) allOutputArtifacts.add(pipeline.outputs.artifact);
65
+ }
66
+
67
+ // ─── Check 2: No dangling sink refs ─────────────────────────────────────────
68
+
69
+ console.log('\n[2] Dangling sink references...');
70
+ for (const { filename, pipeline } of otters) {
71
+ if (!pipeline) continue;
72
+ for (const sink of pipeline.inputs?.sinks ?? []) {
73
+ if (!allOutputSinks.has(sink)) {
74
+ console.error(` FAIL: ${filename} inputs.sinks references '${sink}' but no otter outputs it`);
75
+ errors++;
76
+ } else {
77
+ console.log(` OK : ${filename} <- ${sink}`);
78
+ }
79
+ }
80
+ }
81
+ if (errors === 0) console.log(' (no dangling sinks)');
82
+
83
+ // ─── Check 3: No dangling artifact refs ─────────────────────────────────────
84
+
85
+ console.log('\n[3] Dangling artifact references...');
86
+ let artifactRefs = 0;
87
+ for (const { filename, pipeline } of otters) {
88
+ if (!pipeline) continue;
89
+ for (const art of pipeline.inputs?.artifacts ?? []) {
90
+ artifactRefs++;
91
+ if (!allOutputArtifacts.has(art)) {
92
+ console.error(` FAIL: ${filename} inputs.artifacts references '${art}' but no otter outputs it`);
93
+ errors++;
94
+ } else {
95
+ console.log(` OK : ${filename} <- ${art}`);
96
+ }
97
+ }
98
+ }
99
+ if (artifactRefs === 0) console.log(' (no artifact refs to check)');
100
+
101
+ // ─── Check 4: No cycles (Kahn's algorithm topological sort) ─────────────────
102
+
103
+ console.log('\n[4] Cycle detection (Kahn topological sort)...');
104
+
105
+ // Build adjacency by artifact: producer -> [consumers]
106
+ // Node = otter name
107
+ const otterByOutputArtifact = new Map();
108
+ for (const { name, pipeline } of otters) {
109
+ if (!pipeline) continue;
110
+ if (pipeline.outputs?.artifact) {
111
+ otterByOutputArtifact.set(pipeline.outputs.artifact, name);
112
+ }
113
+ }
114
+
115
+ // Build adjacency list: name -> Set<name> (direct dependencies)
116
+ const deps = new Map();
117
+ for (const { name, pipeline } of otters) {
118
+ if (!pipeline || ORCHESTRATORS.has(name)) continue;
119
+ deps.set(name, new Set());
120
+ for (const art of pipeline.inputs?.artifacts ?? []) {
121
+ const producer = otterByOutputArtifact.get(art);
122
+ if (producer && producer !== name) deps.get(name).add(producer);
123
+ }
124
+ for (const sink of pipeline.inputs?.sinks ?? []) {
125
+ // find which otter outputs this sink
126
+ for (const { name: pName, pipeline: pPipeline } of otters) {
127
+ if (!pPipeline || pName === name) continue;
128
+ if ((pPipeline.outputs?.sinks ?? []).includes(sink)) {
129
+ deps.get(name).add(pName);
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ // Kahn's: compute in-degree, enqueue zero-in-degree nodes
136
+ const inDegree = new Map();
137
+ const adjOut = new Map(); // name -> [names that depend on it]
138
+ for (const name of deps.keys()) {
139
+ inDegree.set(name, 0);
140
+ adjOut.set(name, []);
141
+ }
142
+ for (const [name, upstream] of deps.entries()) {
143
+ for (const u of upstream) {
144
+ inDegree.set(name, (inDegree.get(name) ?? 0) + 1);
145
+ if (!adjOut.has(u)) adjOut.set(u, []);
146
+ adjOut.get(u).push(name);
147
+ }
148
+ }
149
+
150
+ const queue = [];
151
+ for (const [name, deg] of inDegree.entries()) {
152
+ if (deg === 0) queue.push(name);
153
+ }
154
+
155
+ const sorted = [];
156
+ while (queue.length > 0) {
157
+ const node = queue.shift();
158
+ sorted.push(node);
159
+ for (const dependent of adjOut.get(node) ?? []) {
160
+ const newDeg = (inDegree.get(dependent) ?? 1) - 1;
161
+ inDegree.set(dependent, newDeg);
162
+ if (newDeg === 0) queue.push(dependent);
163
+ }
164
+ }
165
+
166
+ if (sorted.length !== deps.size) {
167
+ console.error(` FAIL: cycle detected! Sorted ${sorted.length} of ${deps.size} nodes.`);
168
+ const unsorted = [...deps.keys()].filter((n) => !sorted.includes(n));
169
+ console.error(` Nodes in cycle: ${unsorted.join(', ')}`);
170
+ errors++;
171
+ } else {
172
+ console.log(` OK : topological order (${sorted.length} nodes):`);
173
+ console.log(` ${sorted.join(' -> ')}`);
174
+ }
175
+
176
+ // ─── Result ──────────────────────────────────────────────────────────────────
177
+
178
+ console.log('');
179
+ if (errors > 0) {
180
+ console.error(`SANITY CHECK FAILED — ${errors} error(s). Fix above before committing.`);
181
+ process.exit(1);
182
+ } else {
183
+ console.log('SANITY CHECK PASSED — no dangling refs, no cycles.');
184
+ process.exit(0);
185
+ }
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * strip-artifact-schemas.cjs
4
+ *
5
+ * Removes hardcoded artifact schema JSON blocks from specialist otter system_prompts.
6
+ * These are now injected dynamically via build_specialist_prompt (REQUIRED_ARTIFACT_SCHEMA).
7
+ *
8
+ * Run: node packages/otters/scripts/strip-artifact-schemas.cjs
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const OTTERS_DIR = path.join(__dirname, '..', 'src');
15
+ const REFERENCE_LINE =
16
+ '**Artifact shape:** See the **REQUIRED_ARTIFACT_SCHEMA** section in your prompt for the canonical artifact shape. Use it when calling `stackwright_pro_validate_artifact`.';
17
+
18
+ function stripArtifactSchemaFromArray(promptArray) {
19
+ // Strategy: find the index of the ```json opening fence,
20
+ // then find the matching closing ``` fence.
21
+ // Replace everything from the fence through the closing fence + surrounding context
22
+ // with the REFERENCE_LINE.
23
+
24
+ const result = [];
25
+ let i = 0;
26
+ let stripped = false;
27
+
28
+ while (i < promptArray.length) {
29
+ const item = promptArray[i];
30
+
31
+ // Detect start of a JSON code block fence
32
+ if (typeof item === 'string' && item.trim() === '```json') {
33
+ // Find the closing fence
34
+ let j = i + 1;
35
+ while (j < promptArray.length && promptArray[j].trim() !== '```') {
36
+ j++;
37
+ }
38
+ // j now points to the closing ``` (or end of array)
39
+
40
+ // Replace this entire block with the reference line
41
+ // (but only the first JSON block per otter — the artifact schema block)
42
+ if (!stripped) {
43
+ result.push(REFERENCE_LINE);
44
+ stripped = true;
45
+ }
46
+
47
+ // Skip past the closing fence
48
+ i = j + 1;
49
+ continue;
50
+ }
51
+
52
+ result.push(item);
53
+ i++;
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ function stripArtifactSchemaFromString(str) {
60
+ // For cases where the schema is embedded in a longer string as a ```json...``` block
61
+ // Replace the first ```json ... ``` block with the reference line
62
+ const jsonFenceRegex = /```json[\s\S]*?```/;
63
+ if (jsonFenceRegex.test(str)) {
64
+ return str.replace(jsonFenceRegex, REFERENCE_LINE);
65
+ }
66
+ return str;
67
+ }
68
+
69
+ function processOtterFile(filePath) {
70
+ const raw = fs.readFileSync(filePath, 'utf-8');
71
+ const otter = JSON.parse(raw);
72
+
73
+ if (!Array.isArray(otter.system_prompt)) {
74
+ console.log(` ⚠️ ${path.basename(filePath)}: system_prompt is not an array — skipping`);
75
+ return false;
76
+ }
77
+
78
+ // Check if the artifact schema block is spread across array elements (```json as its own element)
79
+ // or embedded within a longer string
80
+ const hasFenceAsElement = otter.system_prompt.some(
81
+ (s) => typeof s === 'string' && s.trim() === '```json'
82
+ );
83
+
84
+ const hasEmbeddedFence = otter.system_prompt.some(
85
+ (s) => typeof s === 'string' && s.includes('```json')
86
+ );
87
+
88
+ let modified = false;
89
+
90
+ if (hasFenceAsElement) {
91
+ // Multi-element block — strip using array strategy
92
+ const original = JSON.stringify(otter.system_prompt);
93
+ otter.system_prompt = stripArtifactSchemaFromArray(otter.system_prompt);
94
+ if (JSON.stringify(otter.system_prompt) !== original) {
95
+ modified = true;
96
+ }
97
+ } else if (hasEmbeddedFence) {
98
+ // Embedded fence within a string — strip using string replacement
99
+ const original = JSON.stringify(otter.system_prompt);
100
+ otter.system_prompt = otter.system_prompt.map((s) =>
101
+ typeof s === 'string' ? stripArtifactSchemaFromString(s) : s
102
+ );
103
+ if (JSON.stringify(otter.system_prompt) !== original) {
104
+ modified = true;
105
+ }
106
+ }
107
+
108
+ if (modified) {
109
+ fs.writeFileSync(filePath, JSON.stringify(otter, null, 2) + '\n');
110
+ console.log(` ✅ ${path.basename(filePath)}: artifact schema block removed`);
111
+ } else {
112
+ console.log(
113
+ ` ℹ️ ${path.basename(filePath)}: no artifact schema block found (may already be clean)`
114
+ );
115
+ }
116
+
117
+ return modified;
118
+ }
119
+
120
+ console.log('Stripping hardcoded artifact schema blocks from specialist otters...\n');
121
+
122
+ const otterFiles = fs
123
+ .readdirSync(OTTERS_DIR)
124
+ .filter((f) => f.endsWith('-otter.json') && f !== 'stackwright-pro-foreman-otter.json')
125
+ .map((f) => path.join(OTTERS_DIR, f));
126
+
127
+ let totalModified = 0;
128
+ for (const filePath of otterFiles) {
129
+ const modified = processOtterFile(filePath);
130
+ if (modified) totalModified++;
131
+ }
132
+
133
+ console.log(`\nDone. ${totalModified} otter file(s) modified.`);
@@ -1,15 +1,21 @@
1
1
  {
2
2
  "version": "1.0",
3
3
  "algorithm": "sha256",
4
- "generated": "2026-04-16T15:32:31.051Z",
5
4
  "files": {
6
- "stackwright-pro-api-otter.json": "ad0c3694af41000420229edce4108f860eaa58ab321f8618565d03ebce80bcac",
7
- "stackwright-pro-auth-otter.json": "e8e02ef1389e0d5e55bfa6d960a050ab976bf7960fda4ae805675020874ce4c6",
8
- "stackwright-pro-dashboard-otter.json": "0b4100afef4946bae259f5759aea872d7b1a25a00af191e1ead32bf9ee304d08",
9
- "stackwright-pro-data-otter.json": "38ae3a26f064499a5f9773dfea1e2c21f9f358207110224a8e94c19443d236f1",
10
- "stackwright-pro-designer-otter.json": "46c9fd94a46f1a3f5267f4cb70c3db0adfc28dc7d4ac50256cbe40ea5363b4f0",
11
- "stackwright-pro-foreman-otter.json": "73f0913de78dfd3da726d9a32c63d84961985109c3ffc5c7522f6c63559609e7",
12
- "stackwright-pro-page-otter.json": "0973f1b75a481fd177c5ada1a965f8c32e07f97fc28bbbf03b51d9e6d2af2f74",
13
- "stackwright-pro-theme-otter.json": "2cca6eab4d7ee226d3fca488e908be6ace28dbaea9dae1b111cb76608f0747e6"
5
+ "stackwright-pro-api-otter.json": "18112d603646457cecdfd57851109ff9f9ff8f402646e0d54ddef88cf5547d91",
6
+ "stackwright-pro-auth-otter.json": "1a21d9f23e250f23291124307e5a2a32af5a716566319a3290372dae3d02e637",
7
+ "stackwright-pro-dashboard-otter.json": "f8d594b14e05efea30cf253052d7c7df3e8c4174d739be4cd33f9c96cecdc451",
8
+ "stackwright-pro-data-otter.json": "1ad3ed99bbe7b550f654c679a8c0ea3363b2c52031042cd177c6e5f9e1c50a21",
9
+ "stackwright-pro-designer-otter.json": "69a6c09fbb54f971c48eae7fd52faa63cf79be2923e435aca9ff802e8d541d0f",
10
+ "stackwright-pro-domain-expert-otter.json": "14d77cade020f44d1b5a2b406e2599501f3466ee90ca4248500a441d419bc59c",
11
+ "stackwright-pro-foreman-otter.json": "9d68b7b20af7a8a1668e2a693b4a2b05cf37acce1e9f6298a0966c97a3417e9f",
12
+ "stackwright-pro-form-wizard-otter.json": "7f6f94192f26face57dc8b2e22e0a49d23ceeeb356ca4ebde05492f1b25e3c47",
13
+ "stackwright-pro-geo-otter.json": "fc3d18e02a6147d95d3dd9093ce74c0e8fffaecc2f9ecdbd19c163a80129d264",
14
+ "stackwright-pro-page-otter.json": "879e8ff4aa645bac2c63dc962cad1a549876983c257aec756e95293c3b22ce7e",
15
+ "stackwright-pro-polish-otter.json": "fd8f8963266c6179eebf8eac4f656e8274aa3a721e5296abdbde5b00ad8a2297",
16
+ "stackwright-pro-qa-otter.json": "8e6007e18687b6b023f2c40f5517937a857da3f31e4e423cc308493ed601793c",
17
+ "stackwright-pro-scaffold-otter.json": "92930c732547c90a19e89ee0e25b5f037b7366f770941f0a01e148ff240a1b6d",
18
+ "stackwright-pro-theme-otter.json": "01d16d0597d475db60362e3b8020035e140817d197c36f4307a48b95f4b28b60",
19
+ "stackwright-services-otter.json": "2a50ceb3fad166251d0ad8709a34a32118645713e0e34628165d16dced1d5c93"
14
20
  }
15
21
  }