wogiflow 2.26.2 → 2.29.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.
- package/.claude/commands/wogi-bug.md +30 -0
- package/.claude/commands/wogi-debug-hypothesis.md +33 -0
- package/.claude/commands/wogi-morning.md +1 -2
- package/.claude/commands/wogi-review.md +31 -2
- package/.claude/commands/wogi-start.md +32 -0
- package/.claude/commands/wogi-statusline-setup.md +12 -0
- package/.claude/commands/wogi-story.md +3 -2
- package/.claude/docs/claude-code-compatibility.md +40 -0
- package/.claude/docs/phases/01-explore.md +2 -1
- package/.claude/docs/phases/03-implement.md +4 -0
- package/.claude/docs/phases/04-verify.md +45 -0
- package/.claude/rules/README.md +36 -0
- package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
- package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
- package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
- package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
- package/.claude/rules/alternative-short-name.md +12 -0
- package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
- package/.claude/rules/architecture/hook-three-layer.md +68 -0
- package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
- package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
- package/.claude/settings.json +1 -1
- package/.workflow/agents/logic-adversary.md +2 -1
- package/.workflow/agents/personas/README.md +48 -0
- package/.workflow/agents/personas/platform-rigor.md +38 -0
- package/.workflow/agents/personas/scale-skeptic.md +28 -0
- package/.workflow/agents/personas/security-hawk.md +34 -0
- package/.workflow/agents/personas/simplicity-champion.md +37 -0
- package/.workflow/agents/personas/user-advocate.md +36 -0
- package/.workflow/bridges/base-bridge.js +46 -23
- package/.workflow/templates/claude-md.hbs +44 -122
- package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
- package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
- package/.workflow/templates/partials/methodology-rules.hbs +85 -79
- package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
- package/lib/fuzzy-patch.js +251 -0
- package/lib/installer.js +8 -0
- package/lib/memory-proposal-store.js +458 -0
- package/lib/mode-schema.js +255 -0
- package/lib/skill-proposal-store.js +432 -0
- package/lib/skill-registry.js +1 -1
- package/lib/wogi-claude +149 -9
- package/lib/wogi-claude-expect.exp +113 -76
- package/lib/workspace-channel-server.js +19 -0
- package/lib/workspace-contracts.js +1 -1
- package/lib/workspace-dispatch-tracking.js +144 -0
- package/lib/workspace-gates.js +1 -1
- package/lib/workspace-ipc-sqlite.js +550 -0
- package/lib/workspace-messages.js +92 -0
- package/lib/workspace-routing.js +1 -1
- package/lib/workspace-task-injector.js +223 -0
- package/lib/workspace.js +23 -0
- package/lib/worktree-review.js +315 -0
- package/package.json +2 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +28 -4
- package/scripts/flow-ac-scope-preservation.js +238 -0
- package/scripts/flow-auto-review-worker.js +75 -0
- package/scripts/flow-auto-review.js +102 -0
- package/scripts/flow-autonomous-detector.js +118 -0
- package/scripts/flow-autonomous-mode.js +153 -0
- package/scripts/flow-best-of-n.js +1 -1
- package/scripts/flow-bulk-loop.js +1 -1
- package/scripts/flow-checkpoint.js +2 -6
- package/scripts/flow-community-sync.js +1 -1
- package/scripts/flow-completion-summary.js +176 -0
- package/scripts/flow-completion-truth-gate.js +343 -4
- package/scripts/flow-config-defaults.js +52 -5
- package/scripts/flow-config-loader.js +3 -2
- package/scripts/flow-context-compact/expander.js +1 -1
- package/scripts/flow-context-compact/section-extractor.js +2 -2
- package/scripts/flow-context-gatherer.js +1 -1
- package/scripts/flow-context-generator.js +1 -1
- package/scripts/flow-context-scoring.js +1 -1
- package/scripts/flow-correct.js +1 -1
- package/scripts/flow-correction-detector.js +3 -2
- package/scripts/flow-decision-authority.js +66 -15
- package/scripts/flow-done.js +33 -1
- package/scripts/flow-epic-cascade.js +171 -0
- package/scripts/flow-epics.js +2 -7
- package/scripts/flow-eval-judge.js +1 -1
- package/scripts/flow-eval.js +1 -1
- package/scripts/flow-export-scanner.js +2 -6
- package/scripts/flow-failure-learning.js +1 -1
- package/scripts/flow-feature-dossier.js +787 -0
- package/scripts/flow-figma-extract.js +2 -2
- package/scripts/flow-figma-generate.js +1 -1
- package/scripts/flow-gate-confidence.js +1 -1
- package/scripts/flow-health.js +52 -1
- package/scripts/flow-hooks.js +1 -1
- package/scripts/flow-id.js +19 -3
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-knowledge-router.js +1 -1
- package/scripts/flow-knowledge-sync.js +1 -1
- package/scripts/flow-logic-adversary.js +76 -1
- package/scripts/flow-logic-rules.js +380 -0
- package/scripts/flow-long-input.js +5 -5
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +78 -7
- package/scripts/flow-migrate.js +1 -1
- package/scripts/flow-model-caller.js +1 -1
- package/scripts/flow-models.js +2 -2
- package/scripts/flow-morning.js +0 -17
- package/scripts/flow-multi-approach.js +1 -1
- package/scripts/flow-orchestrate-context.js +4 -4
- package/scripts/flow-orchestrate-templates.js +1 -1
- package/scripts/flow-orchestrate.js +8 -8
- package/scripts/flow-peer-review.js +1 -1
- package/scripts/flow-phase.js +9 -0
- package/scripts/flow-proactive-compact.js +1 -1
- package/scripts/flow-prompt-composer.js +3 -2
- package/scripts/flow-prompt-template.js +3 -2
- package/scripts/flow-providers.js +1 -1
- package/scripts/flow-question-queue.js +255 -0
- package/scripts/flow-repo-map.js +312 -0
- package/scripts/flow-review-passes/index.js +1 -1
- package/scripts/flow-review-passes/integration.js +1 -1
- package/scripts/flow-review-passes/structure.js +1 -1
- package/scripts/flow-revision-tracker.js +1 -1
- package/scripts/flow-section-resolver.js +1 -1
- package/scripts/flow-session-end.js +74 -5
- package/scripts/flow-session-state.js +103 -1
- package/scripts/flow-setup-hooks.js +1 -1
- package/scripts/flow-skeptical-evaluator.js +274 -0
- package/scripts/flow-skill-generator.js +3 -3
- package/scripts/flow-skill-learn.js +3 -6
- package/scripts/flow-skill-manage.js +248 -0
- package/scripts/flow-spec-verifier.js +1 -1
- package/scripts/flow-standards-checker.js +75 -0
- package/scripts/flow-standards-gate.js +1 -1
- package/scripts/flow-statusline-setup.js +8 -2
- package/scripts/flow-step-changelog.js +2 -2
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-knowledge.js +1 -1
- package/scripts/flow-step-regression.js +1 -1
- package/scripts/flow-step-simplifier.js +1 -1
- package/scripts/flow-task-analyzer.js +1 -1
- package/scripts/flow-task-classifier.js +1 -1
- package/scripts/flow-task-enforcer.js +1 -1
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/flow-trap-zone.js +1 -1
- package/scripts/flow-utils.js +4 -0
- package/scripts/flow-worker-mcp-strip.js +122 -0
- package/scripts/flow-worker-question-classifier.js +51 -5
- package/scripts/flow-workspace-migrate-ipc.js +216 -0
- package/scripts/flow-workspace-summary.js +256 -0
- package/scripts/hooks/adapters/base-adapter.js +2 -2
- package/scripts/hooks/core/feature-dossier-gate.js +194 -0
- package/scripts/hooks/core/observation-capture.js +24 -0
- package/scripts/hooks/core/overdue-dispatches.js +20 -1
- package/scripts/hooks/core/phase-gate.js +15 -1
- package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
- package/scripts/hooks/core/post-compact.js +5 -2
- package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
- package/scripts/hooks/core/routing-gate.js +58 -0
- package/scripts/hooks/core/session-context.js +108 -0
- package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
- package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
- package/scripts/hooks/core/session-end.js +25 -0
- package/scripts/hooks/core/setup-handler.js +1 -1
- package/scripts/hooks/core/task-boundary-reset.js +110 -4
- package/scripts/hooks/core/worker-boundary-gate.js +71 -0
- package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
- package/scripts/hooks/entry/claude-code/session-start.js +74 -30
- package/scripts/hooks/entry/claude-code/stop.js +47 -1
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
- package/.workflow/templates/partials/user-commands.hbs +0 -20
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Skill Proposal Store
|
|
5
|
+
*
|
|
6
|
+
* Durable storage for agent-proposed skill changes (new / edit / remove).
|
|
7
|
+
* Proposals are staged until the user reviews them at session-end and approves
|
|
8
|
+
* or rejects. Approved proposals are applied by `promote`; rejected proposals
|
|
9
|
+
* are removed without touching `.claude/skills/`.
|
|
10
|
+
*
|
|
11
|
+
* Storage layout:
|
|
12
|
+
* .workflow/state/skill-proposals.json — proposal records (array)
|
|
13
|
+
* .claude/skills/pending/<name>.md — staged content for propose/patch
|
|
14
|
+
* .claude/skills/archived/<name>.md — destination after approved remove
|
|
15
|
+
*
|
|
16
|
+
* Record schema:
|
|
17
|
+
* {
|
|
18
|
+
* id: "prop-<8hex>",
|
|
19
|
+
* action: "propose" | "patch" | "remove",
|
|
20
|
+
* skillName: string,
|
|
21
|
+
* contentPath: string | null, // repo-relative; null for remove
|
|
22
|
+
* rationale: string,
|
|
23
|
+
* proposedAt: ISO-8601 timestamp,
|
|
24
|
+
* proposedBy: "agent" | "user",
|
|
25
|
+
* status: "pending" | "approved" | "rejected"
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
const crypto = require('node:crypto');
|
|
32
|
+
|
|
33
|
+
const { PATHS, isPathWithinProject } = require('../scripts/flow-paths');
|
|
34
|
+
const { safeJsonParse } = require('../scripts/flow-io');
|
|
35
|
+
const { applyFuzzyPatch, DEFAULT_THRESHOLD } = require('./fuzzy-patch');
|
|
36
|
+
|
|
37
|
+
const PROPOSALS_FILE = path.join(PATHS.state, 'skill-proposals.json');
|
|
38
|
+
const PENDING_DIR = path.join(PATHS.claude, 'skills', 'pending');
|
|
39
|
+
const ARCHIVED_DIR = path.join(PATHS.claude, 'skills', 'archived');
|
|
40
|
+
const ACTIVE_SKILLS_DIR = path.join(PATHS.claude, 'skills');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read `skills.fuzzyPatchThreshold` from .workflow/config.json. Falls back to
|
|
44
|
+
* the library default (0.85) on any error. Config read is deferred so test
|
|
45
|
+
* harnesses can point at temp project roots without a stale cache.
|
|
46
|
+
*/
|
|
47
|
+
function getFuzzyPatchThreshold() {
|
|
48
|
+
const configPath = path.join(PATHS.root, '.workflow', 'config.json');
|
|
49
|
+
const cfg = safeJsonParse(configPath, null);
|
|
50
|
+
const v = cfg && cfg.skills && cfg.skills.fuzzyPatchThreshold;
|
|
51
|
+
if (typeof v === 'number' && v >= 0 && v <= 1) return v;
|
|
52
|
+
return DEFAULT_THRESHOLD;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const VALID_SKILL_NAME = /^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)*$/;
|
|
56
|
+
const VALID_ACTIONS = new Set(['propose', 'patch', 'remove']);
|
|
57
|
+
|
|
58
|
+
// ============================================================
|
|
59
|
+
// Low-level helpers
|
|
60
|
+
// ============================================================
|
|
61
|
+
|
|
62
|
+
function ensureDir(dir) {
|
|
63
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readProposals() {
|
|
67
|
+
if (!fs.existsSync(PROPOSALS_FILE)) return [];
|
|
68
|
+
try {
|
|
69
|
+
const raw = fs.readFileSync(PROPOSALS_FILE, 'utf-8');
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
72
|
+
} catch (_err) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeProposals(list) {
|
|
78
|
+
ensureDir(path.dirname(PROPOSALS_FILE));
|
|
79
|
+
fs.writeFileSync(PROPOSALS_FILE, JSON.stringify(list, null, 2) + '\n', 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function generateProposalId() {
|
|
83
|
+
return `prop-${crypto.randomBytes(4).toString('hex')}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateSkillName(name) {
|
|
87
|
+
if (!name || typeof name !== 'string') {
|
|
88
|
+
throw new Error('skill name is required');
|
|
89
|
+
}
|
|
90
|
+
if (!VALID_SKILL_NAME.test(name)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`invalid skill name '${name}': use lowercase letters, digits, hyphens (kebab-case); forward-slashes allowed for nesting`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function skillNameToFile(name) {
|
|
98
|
+
return `${name}.md`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveWithinProject(relOrAbs) {
|
|
102
|
+
const abs = path.isAbsolute(relOrAbs) ? relOrAbs : path.resolve(PATHS.root, relOrAbs);
|
|
103
|
+
if (!isPathWithinProject(abs)) {
|
|
104
|
+
throw new Error(`path escapes project root: ${relOrAbs}`);
|
|
105
|
+
}
|
|
106
|
+
return abs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toRepoRelative(absPath) {
|
|
110
|
+
return path.relative(PATHS.root, absPath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function activeSkillPath(name) {
|
|
114
|
+
return path.join(ACTIVE_SKILLS_DIR, skillNameToFile(name));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function pendingSkillPath(name) {
|
|
118
|
+
return path.join(PENDING_DIR, skillNameToFile(name));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function pendingFindPath(name) {
|
|
122
|
+
return path.join(PENDING_DIR, `${name}.find.md`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function pendingReplacePath(name) {
|
|
126
|
+
return path.join(PENDING_DIR, `${name}.replace.md`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function archivedSkillPath(name) {
|
|
130
|
+
return path.join(ARCHIVED_DIR, skillNameToFile(name));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================
|
|
134
|
+
// Public API
|
|
135
|
+
// ============================================================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Stage a proposal record. For propose/patch, copies `contentFile` into
|
|
139
|
+
* .claude/skills/pending/<name>.md. For remove, no content is copied.
|
|
140
|
+
*/
|
|
141
|
+
function createProposal({
|
|
142
|
+
action,
|
|
143
|
+
skillName,
|
|
144
|
+
contentFile = null,
|
|
145
|
+
findFile = null,
|
|
146
|
+
replaceFile = null,
|
|
147
|
+
rationale = '',
|
|
148
|
+
proposedBy = 'agent',
|
|
149
|
+
}) {
|
|
150
|
+
if (!VALID_ACTIONS.has(action)) {
|
|
151
|
+
throw new Error(`invalid action '${action}': expected propose|patch|remove`);
|
|
152
|
+
}
|
|
153
|
+
validateSkillName(skillName);
|
|
154
|
+
|
|
155
|
+
let contentPath = null;
|
|
156
|
+
let findPath = null;
|
|
157
|
+
let replacePath = null;
|
|
158
|
+
let patchMode = null;
|
|
159
|
+
|
|
160
|
+
if (action === 'propose') {
|
|
161
|
+
if (!contentFile) throw new Error(`--content is required for ${action}`);
|
|
162
|
+
const srcAbs = resolveWithinProject(contentFile);
|
|
163
|
+
if (!fs.existsSync(srcAbs)) throw new Error(`content file not found: ${contentFile}`);
|
|
164
|
+
ensureDir(PENDING_DIR);
|
|
165
|
+
const destAbs = pendingSkillPath(skillName);
|
|
166
|
+
ensureDir(path.dirname(destAbs));
|
|
167
|
+
fs.copyFileSync(srcAbs, destAbs);
|
|
168
|
+
contentPath = toRepoRelative(destAbs);
|
|
169
|
+
} else if (action === 'patch') {
|
|
170
|
+
// Two mutually-exclusive modes:
|
|
171
|
+
// 1. full-replace: --content <file> (legacy / F1 behavior)
|
|
172
|
+
// 2. fuzzy-patch: --find <file> --replace <file> (F3)
|
|
173
|
+
const hasFuzzy = Boolean(findFile || replaceFile);
|
|
174
|
+
const hasFull = Boolean(contentFile);
|
|
175
|
+
if (hasFuzzy && hasFull) {
|
|
176
|
+
throw new Error('patch accepts either --content OR --find/--replace, not both');
|
|
177
|
+
}
|
|
178
|
+
if (!hasFuzzy && !hasFull) {
|
|
179
|
+
throw new Error('patch requires --content, or --find + --replace');
|
|
180
|
+
}
|
|
181
|
+
ensureDir(PENDING_DIR);
|
|
182
|
+
|
|
183
|
+
if (hasFull) {
|
|
184
|
+
patchMode = 'full-replace';
|
|
185
|
+
const srcAbs = resolveWithinProject(contentFile);
|
|
186
|
+
if (!fs.existsSync(srcAbs)) throw new Error(`content file not found: ${contentFile}`);
|
|
187
|
+
const destAbs = pendingSkillPath(skillName);
|
|
188
|
+
ensureDir(path.dirname(destAbs));
|
|
189
|
+
fs.copyFileSync(srcAbs, destAbs);
|
|
190
|
+
contentPath = toRepoRelative(destAbs);
|
|
191
|
+
} else {
|
|
192
|
+
patchMode = 'fuzzy';
|
|
193
|
+
if (!findFile || !replaceFile) {
|
|
194
|
+
throw new Error('fuzzy patch requires both --find and --replace');
|
|
195
|
+
}
|
|
196
|
+
const findAbs = resolveWithinProject(findFile);
|
|
197
|
+
const replAbs = resolveWithinProject(replaceFile);
|
|
198
|
+
if (!fs.existsSync(findAbs)) throw new Error(`find file not found: ${findFile}`);
|
|
199
|
+
if (!fs.existsSync(replAbs)) throw new Error(`replace file not found: ${replaceFile}`);
|
|
200
|
+
// Active skill must exist to fuzzy-patch against.
|
|
201
|
+
const activeAbs = activeSkillPath(skillName);
|
|
202
|
+
if (!fs.existsSync(activeAbs)) {
|
|
203
|
+
throw new Error(`cannot stage fuzzy patch — no active skill at ${toRepoRelative(activeAbs)}`);
|
|
204
|
+
}
|
|
205
|
+
const findDest = pendingFindPath(skillName);
|
|
206
|
+
const replDest = pendingReplacePath(skillName);
|
|
207
|
+
ensureDir(path.dirname(findDest));
|
|
208
|
+
fs.copyFileSync(findAbs, findDest);
|
|
209
|
+
fs.copyFileSync(replAbs, replDest);
|
|
210
|
+
findPath = toRepoRelative(findDest);
|
|
211
|
+
replacePath = toRepoRelative(replDest);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (action === 'remove') {
|
|
216
|
+
const activeAbs = activeSkillPath(skillName);
|
|
217
|
+
if (!fs.existsSync(activeAbs)) {
|
|
218
|
+
throw new Error(`cannot propose remove — no active skill at ${toRepoRelative(activeAbs)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const record = {
|
|
223
|
+
id: generateProposalId(),
|
|
224
|
+
action,
|
|
225
|
+
skillName,
|
|
226
|
+
contentPath,
|
|
227
|
+
findPath,
|
|
228
|
+
replacePath,
|
|
229
|
+
patchMode,
|
|
230
|
+
rationale: String(rationale || ''),
|
|
231
|
+
proposedAt: new Date().toISOString(),
|
|
232
|
+
proposedBy: proposedBy === 'user' ? 'user' : 'agent',
|
|
233
|
+
status: 'pending',
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const list = readProposals();
|
|
237
|
+
list.push(record);
|
|
238
|
+
writeProposals(list);
|
|
239
|
+
|
|
240
|
+
return record;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function listProposals(filter = {}) {
|
|
244
|
+
const list = readProposals();
|
|
245
|
+
if (!filter || Object.keys(filter).length === 0) return list;
|
|
246
|
+
return list.filter((r) =>
|
|
247
|
+
Object.entries(filter).every(([k, v]) => r[k] === v)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function findProposal({ id = null, skillName = null, action = null, status = 'pending' }) {
|
|
252
|
+
const list = readProposals();
|
|
253
|
+
return list.find((r) =>
|
|
254
|
+
(id ? r.id === id : true) &&
|
|
255
|
+
(skillName ? r.skillName === skillName : true) &&
|
|
256
|
+
(action ? r.action === action : true) &&
|
|
257
|
+
(status ? r.status === status : true)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function updateProposalStatus(id, status) {
|
|
262
|
+
const list = readProposals();
|
|
263
|
+
const idx = list.findIndex((r) => r.id === id);
|
|
264
|
+
if (idx < 0) throw new Error(`no proposal with id ${id}`);
|
|
265
|
+
list[idx].status = status;
|
|
266
|
+
list[idx].decidedAt = new Date().toISOString();
|
|
267
|
+
writeProposals(list);
|
|
268
|
+
return list[idx];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Apply a pending proposal (user-invoked only).
|
|
273
|
+
* propose → move pending/<name>.md → .claude/skills/<name>.md
|
|
274
|
+
* patch → overwrite .claude/skills/<name>.md with pending content
|
|
275
|
+
* remove → move .claude/skills/<name>.md → .claude/skills/archived/<name>.md
|
|
276
|
+
*
|
|
277
|
+
* Selector: `skillName` (finds the most recent pending proposal for that skill)
|
|
278
|
+
* or `id` (exact proposal id). `id` wins when both provided.
|
|
279
|
+
*/
|
|
280
|
+
function promoteProposal({ skillName = null, id = null } = {}) {
|
|
281
|
+
if (!skillName && !id) throw new Error('promoteProposal requires skillName or id');
|
|
282
|
+
const proposal = id
|
|
283
|
+
? findProposal({ id })
|
|
284
|
+
: findProposal({ skillName });
|
|
285
|
+
if (!proposal) {
|
|
286
|
+
throw new Error(`no pending proposal found for ${id || skillName}`);
|
|
287
|
+
}
|
|
288
|
+
if (proposal.status !== 'pending') {
|
|
289
|
+
throw new Error(`proposal ${proposal.id} already ${proposal.status}`);
|
|
290
|
+
}
|
|
291
|
+
validateSkillName(proposal.skillName);
|
|
292
|
+
|
|
293
|
+
const activeAbs = activeSkillPath(proposal.skillName);
|
|
294
|
+
const pendingAbs = pendingSkillPath(proposal.skillName);
|
|
295
|
+
const archivedAbs = archivedSkillPath(proposal.skillName);
|
|
296
|
+
|
|
297
|
+
if (proposal.action === 'propose') {
|
|
298
|
+
if (fs.existsSync(activeAbs)) {
|
|
299
|
+
throw new Error(`cannot promote propose — active skill already exists at ${toRepoRelative(activeAbs)}; use patch instead`);
|
|
300
|
+
}
|
|
301
|
+
if (!fs.existsSync(pendingAbs)) {
|
|
302
|
+
throw new Error(`pending content missing at ${toRepoRelative(pendingAbs)}`);
|
|
303
|
+
}
|
|
304
|
+
ensureDir(path.dirname(activeAbs));
|
|
305
|
+
fs.renameSync(pendingAbs, activeAbs);
|
|
306
|
+
} else if (proposal.action === 'patch') {
|
|
307
|
+
if (!fs.existsSync(activeAbs)) {
|
|
308
|
+
throw new Error(`cannot promote patch — no active skill at ${toRepoRelative(activeAbs)}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (proposal.patchMode === 'fuzzy') {
|
|
312
|
+
const findAbs = pendingFindPath(proposal.skillName);
|
|
313
|
+
const replAbs = pendingReplacePath(proposal.skillName);
|
|
314
|
+
if (!fs.existsSync(findAbs) || !fs.existsSync(replAbs)) {
|
|
315
|
+
throw new Error(`pending fuzzy-patch blobs missing for ${proposal.skillName}`);
|
|
316
|
+
}
|
|
317
|
+
const haystack = fs.readFileSync(activeAbs, 'utf-8');
|
|
318
|
+
const find = fs.readFileSync(findAbs, 'utf-8');
|
|
319
|
+
const replace = fs.readFileSync(replAbs, 'utf-8');
|
|
320
|
+
const threshold = getFuzzyPatchThreshold();
|
|
321
|
+
const result = applyFuzzyPatch(haystack, find, replace, { threshold });
|
|
322
|
+
if (!result.applied) {
|
|
323
|
+
// Atomic rejection — leave active skill untouched, leave proposal
|
|
324
|
+
// pending so the user can inspect or reject it manually.
|
|
325
|
+
throw new Error(
|
|
326
|
+
`fuzzy patch rejected for '${proposal.skillName}': ${result.reason} ` +
|
|
327
|
+
`(threshold ${threshold})`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
fs.writeFileSync(activeAbs, result.result, 'utf-8');
|
|
331
|
+
try { fs.unlinkSync(findAbs); } catch (_err) { /* non-critical */ }
|
|
332
|
+
try { fs.unlinkSync(replAbs); } catch (_err) { /* non-critical */ }
|
|
333
|
+
} else {
|
|
334
|
+
// full-replace (legacy / F1 behavior, default when patchMode is absent)
|
|
335
|
+
if (!fs.existsSync(pendingAbs)) {
|
|
336
|
+
throw new Error(`pending content missing at ${toRepoRelative(pendingAbs)}`);
|
|
337
|
+
}
|
|
338
|
+
fs.copyFileSync(pendingAbs, activeAbs);
|
|
339
|
+
fs.unlinkSync(pendingAbs);
|
|
340
|
+
}
|
|
341
|
+
} else if (proposal.action === 'remove') {
|
|
342
|
+
if (!fs.existsSync(activeAbs)) {
|
|
343
|
+
throw new Error(`cannot promote remove — active skill gone at ${toRepoRelative(activeAbs)}`);
|
|
344
|
+
}
|
|
345
|
+
ensureDir(ARCHIVED_DIR);
|
|
346
|
+
ensureDir(path.dirname(archivedAbs));
|
|
347
|
+
fs.renameSync(activeAbs, archivedAbs);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return updateProposalStatus(proposal.id, 'approved');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function rejectProposal({ skillName = null, id = null } = {}) {
|
|
354
|
+
if (!skillName && !id) throw new Error('rejectProposal requires skillName or id');
|
|
355
|
+
const proposal = id ? findProposal({ id }) : findProposal({ skillName });
|
|
356
|
+
if (!proposal) {
|
|
357
|
+
throw new Error(`no pending proposal found for ${id || skillName}`);
|
|
358
|
+
}
|
|
359
|
+
if (proposal.status !== 'pending') {
|
|
360
|
+
throw new Error(`proposal ${proposal.id} already ${proposal.status}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Clean up staged content for propose/patch
|
|
364
|
+
if (proposal.action === 'propose' || proposal.action === 'patch') {
|
|
365
|
+
const pendingAbs = pendingSkillPath(proposal.skillName);
|
|
366
|
+
if (fs.existsSync(pendingAbs)) {
|
|
367
|
+
try { fs.unlinkSync(pendingAbs); } catch (_err) { /* non-critical */ }
|
|
368
|
+
}
|
|
369
|
+
if (proposal.patchMode === 'fuzzy') {
|
|
370
|
+
const findAbs = pendingFindPath(proposal.skillName);
|
|
371
|
+
const replAbs = pendingReplacePath(proposal.skillName);
|
|
372
|
+
if (fs.existsSync(findAbs)) { try { fs.unlinkSync(findAbs); } catch (_err) {} }
|
|
373
|
+
if (fs.existsSync(replAbs)) { try { fs.unlinkSync(replAbs); } catch (_err) {} }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return updateProposalStatus(proposal.id, 'rejected');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Direct archival of an active skill. User-invoked; bypasses proposal staging.
|
|
382
|
+
*/
|
|
383
|
+
function archiveSkill(skillName) {
|
|
384
|
+
validateSkillName(skillName);
|
|
385
|
+
const activeAbs = activeSkillPath(skillName);
|
|
386
|
+
if (!fs.existsSync(activeAbs)) {
|
|
387
|
+
throw new Error(`no active skill at ${toRepoRelative(activeAbs)}`);
|
|
388
|
+
}
|
|
389
|
+
const archivedAbs = archivedSkillPath(skillName);
|
|
390
|
+
ensureDir(path.dirname(archivedAbs));
|
|
391
|
+
fs.renameSync(activeAbs, archivedAbs);
|
|
392
|
+
return { skillName, archivedPath: toRepoRelative(archivedAbs) };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ============================================================
|
|
396
|
+
// Exports
|
|
397
|
+
// ============================================================
|
|
398
|
+
|
|
399
|
+
module.exports = {
|
|
400
|
+
// Core API
|
|
401
|
+
createProposal,
|
|
402
|
+
listProposals,
|
|
403
|
+
findProposal,
|
|
404
|
+
promoteProposal,
|
|
405
|
+
rejectProposal,
|
|
406
|
+
archiveSkill,
|
|
407
|
+
|
|
408
|
+
// Path helpers (for tests & hooks)
|
|
409
|
+
pathFor: {
|
|
410
|
+
proposals: PROPOSALS_FILE,
|
|
411
|
+
pending: PENDING_DIR,
|
|
412
|
+
archived: ARCHIVED_DIR,
|
|
413
|
+
active: ACTIVE_SKILLS_DIR,
|
|
414
|
+
activeSkill: activeSkillPath,
|
|
415
|
+
pendingSkill: pendingSkillPath,
|
|
416
|
+
archivedSkill: archivedSkillPath,
|
|
417
|
+
pendingFind: pendingFindPath,
|
|
418
|
+
pendingReplace: pendingReplacePath,
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
// Config access (exposed for tests)
|
|
422
|
+
getFuzzyPatchThreshold,
|
|
423
|
+
|
|
424
|
+
// Low-level helpers (exposed for targeted tests)
|
|
425
|
+
_internal: {
|
|
426
|
+
readProposals,
|
|
427
|
+
writeProposals,
|
|
428
|
+
updateProposalStatus,
|
|
429
|
+
validateSkillName,
|
|
430
|
+
VALID_ACTIONS,
|
|
431
|
+
},
|
|
432
|
+
};
|
package/lib/skill-registry.js
CHANGED
package/lib/wogi-claude
CHANGED
|
@@ -56,11 +56,13 @@ WOGI_EXPECT_SCRIPT="$WOGI_CLAUDE_DIR/wogi-claude-expect.exp"
|
|
|
56
56
|
# default remains opt-in — the v2.22.3 regression (expect's text match miss on
|
|
57
57
|
# Ink ANSI output) is bounded to users who explicitly asked for expect.
|
|
58
58
|
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
# keystrokes
|
|
59
|
+
# v2.26.3: the expect script now uses `interact -o -re` from second zero —
|
|
60
|
+
# stdin flows to claude throughout, so user keystrokes are NEVER captured
|
|
61
|
+
# by expect. The v2.26.2 approach (rolling buffer + ANSI strip in an expect
|
|
62
|
+
# watch block) had a worse failure mode: on match miss, expect owned stdin
|
|
63
|
+
# for 30s and user keystrokes went into its buffer, making the dialog
|
|
64
|
+
# appear frozen. The new interact approach falls back gracefully — mismatch
|
|
65
|
+
# just means the user dismisses the dialog themselves, no black hole.
|
|
64
66
|
|
|
65
67
|
__wogi_is_worker=0
|
|
66
68
|
if [ -n "${WOGI_WORKSPACE_ROOT:-}" ] && [ -n "${WOGI_REPO_NAME:-}" ] && \
|
|
@@ -68,6 +70,132 @@ if [ -n "${WOGI_WORKSPACE_ROOT:-}" ] && [ -n "${WOGI_REPO_NAME:-}" ] && \
|
|
|
68
70
|
__wogi_is_worker=1
|
|
69
71
|
fi
|
|
70
72
|
|
|
73
|
+
# --- wf-8294d960 (Story A): worker MCP-stripping + init banner ---
|
|
74
|
+
#
|
|
75
|
+
# Root cause (measured 2026-04-24): cold-boot of claude --print in a project
|
|
76
|
+
# with claude.ai OAuth + 7 integrations takes 10-20s; claude --bare --print is
|
|
77
|
+
# <1s. The ~10-19s gap is Claude Code's init (OAuth + remote MCP handshakes
|
|
78
|
+
# + LSP + plugin sync + CLAUDE.md discovery + background prefetches).
|
|
79
|
+
# WogiFlow's SessionStart hook is ~128ms — <1% of the problem. See
|
|
80
|
+
# .workflow/scratch/wf-8294d960-investigation/root-cause.md for full data.
|
|
81
|
+
#
|
|
82
|
+
# Workers are specialized autonomous code executors; they typically do NOT
|
|
83
|
+
# need Gmail/Slack/Atlassian/etc. integrations for refactoring code. Stripping
|
|
84
|
+
# claude.ai MCP from worker boot saves ~3-7s per restart. Users who genuinely
|
|
85
|
+
# need these integrations per-worker can opt in via the config key below.
|
|
86
|
+
#
|
|
87
|
+
# Opt-in: set config.workspace.inheritClaudeAiMcpIntegrations to true in
|
|
88
|
+
# .workflow/config.json (or env var WOGI_WORKER_INHERIT_MCP=1) to keep
|
|
89
|
+
# claude.ai MCP integrations active in worker mode. Default: strip (fast).
|
|
90
|
+
__wogi_strip_mcp=0
|
|
91
|
+
if [ "$__wogi_is_worker" -eq 1 ]; then
|
|
92
|
+
__wogi_strip_mcp=1
|
|
93
|
+
# Env override wins
|
|
94
|
+
if [ "${WOGI_WORKER_INHERIT_MCP:-}" = "1" ]; then
|
|
95
|
+
__wogi_strip_mcp=0
|
|
96
|
+
elif command -v node >/dev/null 2>&1; then
|
|
97
|
+
# Check config.json; fail-open (strip on any error, matching spec non-negotiable)
|
|
98
|
+
__wogi_config_inherit="$(node -e '
|
|
99
|
+
try {
|
|
100
|
+
const cfg = require(process.cwd() + "/.workflow/config.json");
|
|
101
|
+
process.stdout.write(String(!!(cfg.workspace && cfg.workspace.inheritClaudeAiMcpIntegrations)));
|
|
102
|
+
} catch (_e) { process.stdout.write("false"); }
|
|
103
|
+
' 2>/dev/null)"
|
|
104
|
+
if [ "$__wogi_config_inherit" = "true" ]; then
|
|
105
|
+
__wogi_strip_mcp=0
|
|
106
|
+
fi
|
|
107
|
+
fi
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# Resolve the channel-only MCP config used for stripping. Persistent path
|
|
111
|
+
# so we don't regenerate on every restart; living in .workflow/state/ keeps
|
|
112
|
+
# it alongside other worker state.
|
|
113
|
+
#
|
|
114
|
+
# IMPORTANT — REGRESSION FIX (audit-channel-transport-001):
|
|
115
|
+
#
|
|
116
|
+
# The original Story A (wf-8294d960) wrote `{"mcpServers":{}}` — fully
|
|
117
|
+
# empty — under the (unverified) assumption that workers don't need any
|
|
118
|
+
# MCP servers in worker mode. That assumption was WRONG: it stripped
|
|
119
|
+
# `wogi-workspace-channel` which IS the transport that the manager uses
|
|
120
|
+
# to dispatch tasks to workers via `workspace_send_message` (the manager
|
|
121
|
+
# HTTP-POSTs to the worker's channel-server port; with no MCP server,
|
|
122
|
+
# there's no listener, so dispatches silently fail with "connection
|
|
123
|
+
# refused"). Tier-3 evidence (end-to-end manager→worker dispatch) was
|
|
124
|
+
# never collected; only boot-latency was measured. Story B
|
|
125
|
+
# (wf-ab59f0e4) layered COMPLETION-SUMMARY routing on top of this
|
|
126
|
+
# broken transport without auditing the dependency.
|
|
127
|
+
#
|
|
128
|
+
# The proper fix: extract ONLY the `wogi-workspace-channel` entry from
|
|
129
|
+
# the worker's real `.mcp.json` and write a channel-only config. This
|
|
130
|
+
# preserves Story A's boot-speed win (claude.ai MCP integrations stay
|
|
131
|
+
# stripped) while keeping the workspace transport active. If the
|
|
132
|
+
# worker's `.mcp.json` doesn't define `wogi-workspace-channel` (e.g.
|
|
133
|
+
# this is not a workspace member), fall back to the empty MCP config
|
|
134
|
+
# (the strip is harmless in non-workspace contexts).
|
|
135
|
+
__wogi_empty_mcp_config=""
|
|
136
|
+
if [ "$__wogi_strip_mcp" -eq 1 ]; then
|
|
137
|
+
__wogi_empty_mcp_config="${WOGI_WORKSPACE_ROOT:-$(pwd)}/.workflow/state/worker-channel-only-mcp.json"
|
|
138
|
+
__wogi_member_mcp_path="$(pwd)/.mcp.json"
|
|
139
|
+
if command -v node >/dev/null 2>&1; then
|
|
140
|
+
# Use the dedicated helper (testable; see tests/flow-worker-mcp-strip.test.js).
|
|
141
|
+
# Extracts ONLY the wogi-workspace-channel entry from the worker's real
|
|
142
|
+
# .mcp.json so manager-side workspace_send_message dispatch keeps working.
|
|
143
|
+
__wogi_strip_helper=""
|
|
144
|
+
for __wogi_candidate in \
|
|
145
|
+
"$(dirname "$0")/../scripts/flow-worker-mcp-strip.js" \
|
|
146
|
+
"$(npm root -g 2>/dev/null)/wogiflow/scripts/flow-worker-mcp-strip.js" \
|
|
147
|
+
"$(pwd)/node_modules/wogiflow/scripts/flow-worker-mcp-strip.js"; do
|
|
148
|
+
if [ -f "$__wogi_candidate" ]; then
|
|
149
|
+
__wogi_strip_helper="$__wogi_candidate"
|
|
150
|
+
break
|
|
151
|
+
fi
|
|
152
|
+
done
|
|
153
|
+
if [ -n "$__wogi_strip_helper" ]; then
|
|
154
|
+
node "$__wogi_strip_helper" "$__wogi_member_mcp_path" "$__wogi_empty_mcp_config" 2>/dev/null || __wogi_strip_mcp=0
|
|
155
|
+
else
|
|
156
|
+
# Helper not found — fall back to inline extraction (legacy code path
|
|
157
|
+
# for installs that pre-date the helper script).
|
|
158
|
+
node -e '
|
|
159
|
+
const fs = require("fs");
|
|
160
|
+
const path = require("path");
|
|
161
|
+
const [src, out] = process.argv.slice(1);
|
|
162
|
+
let channelEntry = null;
|
|
163
|
+
try {
|
|
164
|
+
if (fs.existsSync(src)) {
|
|
165
|
+
const cfg = JSON.parse(fs.readFileSync(src, "utf-8"));
|
|
166
|
+
const ws = cfg && cfg.mcpServers && cfg.mcpServers["wogi-workspace-channel"];
|
|
167
|
+
if (ws) channelEntry = ws;
|
|
168
|
+
}
|
|
169
|
+
} catch (_e) {}
|
|
170
|
+
const payload = channelEntry
|
|
171
|
+
? { mcpServers: { "wogi-workspace-channel": channelEntry } }
|
|
172
|
+
: { mcpServers: {} };
|
|
173
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
174
|
+
const tmp = out + ".tmp." + process.pid;
|
|
175
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n");
|
|
176
|
+
fs.renameSync(tmp, out);
|
|
177
|
+
' "$__wogi_member_mcp_path" "$__wogi_empty_mcp_config" 2>/dev/null || __wogi_strip_mcp=0
|
|
178
|
+
fi
|
|
179
|
+
else
|
|
180
|
+
# node missing — last-resort fallback. Empty config = manager dispatch
|
|
181
|
+
# will fail, but worker boot will still succeed.
|
|
182
|
+
mkdir -p "$(dirname "$__wogi_empty_mcp_config")" 2>/dev/null
|
|
183
|
+
printf '{"mcpServers":{}}\n' > "$__wogi_empty_mcp_config" 2>/dev/null || __wogi_strip_mcp=0
|
|
184
|
+
fi
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
# Init banner (C-2): workers take ~5-10s even with MCP stripped; tell the user
|
|
188
|
+
# it's not frozen. Interactive solo users don't see this (only workers).
|
|
189
|
+
if [ "$__wogi_is_worker" -eq 1 ] && [ -t 2 ]; then
|
|
190
|
+
__wogi_banner_msg="[wogi-claude] worker '${WOGI_REPO_NAME}' initializing"
|
|
191
|
+
if [ "$__wogi_strip_mcp" -eq 1 ]; then
|
|
192
|
+
__wogi_banner_msg="$__wogi_banner_msg (claude.ai MCP integrations stripped for speed)"
|
|
193
|
+
else
|
|
194
|
+
__wogi_banner_msg="$__wogi_banner_msg (claude.ai MCP inherited — expect +3-7s boot)"
|
|
195
|
+
fi
|
|
196
|
+
echo "$__wogi_banner_msg — expected boot ~5-10s, please wait..." >&2
|
|
197
|
+
fi
|
|
198
|
+
|
|
71
199
|
__wogi_wants_expect=0
|
|
72
200
|
if [ -z "${WOGI_NO_EXPECT:-}" ]; then
|
|
73
201
|
if [ "$__wogi_is_worker" -eq 1 ] || [ "${WOGI_USE_EXPECT:-}" = "1" ]; then
|
|
@@ -105,13 +233,24 @@ if [ "$__wogi_wants_expect" -eq 1 ]; then
|
|
|
105
233
|
fi
|
|
106
234
|
fi
|
|
107
235
|
|
|
236
|
+
# __wogi_claude_args — emit argv with MCP-stripping flags prepended when needed.
|
|
237
|
+
# Uses bash global array so callers expand it with "${__wogi_claude_argv[@]+"${__wogi_claude_argv[@]}"}".
|
|
238
|
+
__wogi_build_argv() {
|
|
239
|
+
__wogi_claude_argv=()
|
|
240
|
+
if [ "$__wogi_strip_mcp" -eq 1 ] && [ -n "$__wogi_empty_mcp_config" ]; then
|
|
241
|
+
__wogi_claude_argv+=(--strict-mcp-config --mcp-config "$__wogi_empty_mcp_config")
|
|
242
|
+
fi
|
|
243
|
+
__wogi_claude_argv+=("$@")
|
|
244
|
+
}
|
|
245
|
+
|
|
108
246
|
# run_claude — invoke claude, routing through expect when we can auto-dismiss
|
|
109
247
|
# the dev-channels dialog. Preserves stdin/stdout/stderr exactly.
|
|
110
248
|
run_claude() {
|
|
249
|
+
__wogi_build_argv "$@"
|
|
111
250
|
if [ "$__wogi_use_expect" -eq 1 ]; then
|
|
112
|
-
expect "$WOGI_EXPECT_SCRIPT" "$CLAUDE_BIN" "
|
|
251
|
+
expect "$WOGI_EXPECT_SCRIPT" "$CLAUDE_BIN" "${__wogi_claude_argv[@]+"${__wogi_claude_argv[@]}"}"
|
|
113
252
|
else
|
|
114
|
-
"$CLAUDE_BIN" "
|
|
253
|
+
"$CLAUDE_BIN" "${__wogi_claude_argv[@]+"${__wogi_claude_argv[@]}"}"
|
|
115
254
|
fi
|
|
116
255
|
}
|
|
117
256
|
|
|
@@ -122,10 +261,11 @@ for arg in "$@"; do
|
|
|
122
261
|
filtered=()
|
|
123
262
|
for a in "$@"; do [ "$a" = "--no-wogi-restart" ] || filtered+=("$a"); done
|
|
124
263
|
CLAUDE_BIN="${WOGI_CLAUDE_BIN:-claude}"
|
|
264
|
+
__wogi_build_argv "${filtered[@]}"
|
|
125
265
|
if [ "$__wogi_use_expect" -eq 1 ]; then
|
|
126
|
-
exec expect "$WOGI_EXPECT_SCRIPT" "$CLAUDE_BIN" "${
|
|
266
|
+
exec expect "$WOGI_EXPECT_SCRIPT" "$CLAUDE_BIN" "${__wogi_claude_argv[@]+"${__wogi_claude_argv[@]}"}"
|
|
127
267
|
else
|
|
128
|
-
exec "$CLAUDE_BIN" "${
|
|
268
|
+
exec "$CLAUDE_BIN" "${__wogi_claude_argv[@]+"${__wogi_claude_argv[@]}"}"
|
|
129
269
|
fi
|
|
130
270
|
fi
|
|
131
271
|
done
|