@entelligentsia/forgecli 0.11.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +324 -0
- package/README.md +2 -1
- package/dist/CHANGELOG-forge-plugin.md +210 -0
- package/dist/bin/forge.js +20 -1
- package/dist/bin/forge.js.map +1 -1
- package/dist/extensions/forgecli/ask-user-tool.js +32 -20
- package/dist/extensions/forgecli/ask-user-tool.js.map +1 -1
- package/dist/extensions/forgecli/config-layer.d.ts +15 -0
- package/dist/extensions/forgecli/config-layer.js +4 -1
- package/dist/extensions/forgecli/config-layer.js.map +1 -1
- package/dist/extensions/forgecli/config-writer.js +4 -1
- package/dist/extensions/forgecli/config-writer.js.map +1 -1
- package/dist/extensions/forgecli/enhance.js +1 -1
- package/dist/extensions/forgecli/enhance.js.map +1 -1
- package/dist/extensions/forgecli/fix-bug.js +31 -1
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-cli-schema.json +19 -0
- package/dist/extensions/forgecli/forge-tools.js +80 -0
- package/dist/extensions/forgecli/forge-tools.js.map +1 -1
- package/dist/extensions/forgecli/forge-update-command.js +24 -18
- package/dist/extensions/forgecli/forge-update-command.js.map +1 -1
- package/dist/extensions/forgecli/friction-emit.d.ts +97 -0
- package/dist/extensions/forgecli/friction-emit.js +246 -0
- package/dist/extensions/forgecli/friction-emit.js.map +1 -0
- package/dist/extensions/forgecli/health-check.d.ts +10 -0
- package/dist/extensions/forgecli/health-check.js +160 -8
- package/dist/extensions/forgecli/health-check.js.map +1 -1
- package/dist/extensions/forgecli/hook-dispatcher.js +24 -2
- package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
- package/dist/extensions/forgecli/hooks/write-guard.js +5 -1
- package/dist/extensions/forgecli/hooks/write-guard.js.map +1 -1
- package/dist/extensions/forgecli/index.js +29 -5
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/lib/store-error-remediation.d.ts +65 -0
- package/dist/extensions/forgecli/lib/store-error-remediation.js +298 -0
- package/dist/extensions/forgecli/lib/store-error-remediation.js.map +1 -0
- package/dist/extensions/forgecli/regenerate.d.ts +22 -0
- package/dist/extensions/forgecli/regenerate.js +133 -3
- package/dist/extensions/forgecli/regenerate.js.map +1 -1
- package/dist/extensions/forgecli/run-sprint.js +16 -1
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.js +30 -8
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/skill-curation-flag.d.ts +21 -0
- package/dist/extensions/forgecli/skill-curation-flag.js +71 -0
- package/dist/extensions/forgecli/skill-curation-flag.js.map +1 -0
- package/dist/extensions/forgecli/skill-curator-subagent.d.ts +101 -0
- package/dist/extensions/forgecli/skill-curator-subagent.js +342 -0
- package/dist/extensions/forgecli/skill-curator-subagent.js.map +1 -0
- package/dist/extensions/forgecli/skill-retriever.d.ts +84 -0
- package/dist/extensions/forgecli/skill-retriever.js +246 -0
- package/dist/extensions/forgecli/skill-retriever.js.map +1 -0
- package/dist/extensions/forgecli/skill-usage-tracker.d.ts +91 -0
- package/dist/extensions/forgecli/skill-usage-tracker.js +224 -0
- package/dist/extensions/forgecli/skill-usage-tracker.js.map +1 -0
- package/dist/extensions/forgecli/store-resolver.d.ts +18 -0
- package/dist/extensions/forgecli/store-resolver.js +44 -4
- package/dist/extensions/forgecli/store-resolver.js.map +1 -1
- package/dist/extensions/forgecli/store-validator.d.ts +3 -0
- package/dist/extensions/forgecli/store-validator.js +4 -2
- package/dist/extensions/forgecli/store-validator.js.map +1 -1
- package/dist/forge-payload/.base-pack/personas/supervisor.md +9 -0
- package/dist/forge-payload/.base-pack/workflows/enhance.md +344 -18
- package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
- package/dist/forge-payload/.schemas/event.schema.json +20 -2
- package/dist/forge-payload/.schemas/migrations.json +112 -0
- package/dist/forge-payload/.schemas/proposal.schema.json +40 -0
- package/dist/forge-payload/agents/store-query-validator.md +103 -0
- package/dist/forge-payload/agents/tomoshibi.md +185 -0
- package/dist/forge-payload/commands/regenerate.md +109 -20
- package/dist/forge-payload/hooks/check-update.js +378 -0
- package/dist/forge-payload/hooks/forge-permissions.js +158 -0
- package/dist/forge-payload/hooks/triage-error.js +71 -0
- package/dist/forge-payload/hooks/validate-write.js +236 -0
- package/dist/forge-payload/integrity.json +32 -0
- package/dist/forge-payload/meta/workflows/meta-enhance.md +344 -18
- package/dist/forge-payload/schemas/structure-manifest.json +511 -0
- package/dist/forge-payload/tools/build-persona-pack.cjs +120 -11
- package/dist/forge-payload/tools/compression-gate.cjs +192 -0
- package/dist/forge-payload/tools/delete-candidate-detector.cjs +114 -0
- package/dist/forge-payload/tools/judge-proposal.cjs +177 -0
- package/dist/forge-payload/tools/manage-versions.cjs +132 -4
- package/dist/forge-payload/tools/queue-drain.cjs +152 -0
- package/dist/forge-payload/tools/replay-scoring.cjs +117 -0
- package/node_modules/@mariozechner/clipboard/package.json +2 -1
- package/node_modules/@mariozechner/clipboard-linux-x64-musl/README.md +3 -0
- package/node_modules/@mariozechner/clipboard-linux-x64-musl/clipboard.linux-x64-musl.node +0 -0
- package/node_modules/@mariozechner/clipboard-linux-x64-musl/package.json +25 -0
- package/package.json +4 -2
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Forge permission auto-approver — runs on PermissionRequest events.
|
|
3
|
+
//
|
|
4
|
+
// Purpose: eliminate the permission prompt storm (BUG-014) by auto-approving
|
|
5
|
+
// known Forge tool patterns and persisting allow rules to localSettings.
|
|
6
|
+
//
|
|
7
|
+
// Protocol (Claude Code PermissionRequest hook):
|
|
8
|
+
// - stdin: JSON envelope { tool_name, tool_input, permission_suggestions }
|
|
9
|
+
// - stdout: { hookSpecificOutput: { hookEventName, decision: { behavior,
|
|
10
|
+
// updatedPermissions } } } to allow and persist rules
|
|
11
|
+
// - exit 0 with no output: let normal permission flow proceed
|
|
12
|
+
// - exit 2 with stderr: block the tool call
|
|
13
|
+
//
|
|
14
|
+
// Security model:
|
|
15
|
+
// - This hook can only ALLOW, never DENY
|
|
16
|
+
// - User deny rules always take precedence over hook allows
|
|
17
|
+
// - Rules persist to .claude/settings.local.json (gitignored, per-project)
|
|
18
|
+
// - Users can inspect/remove rules via /permissions
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
process.on('uncaughtException', (err) => {
|
|
23
|
+
try { process.stderr.write(`forge-permissions: internal error (fail-open): ${err.message}\n`); } catch (_) {}
|
|
24
|
+
process.exit(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ── Pattern registry ──────────────────────────────────────────────
|
|
28
|
+
// Each entry: { pattern: RegExp, rule: string }
|
|
29
|
+
// pattern matches against the tool input string (Bash command, file path, or URL)
|
|
30
|
+
// rule is the allow rule content to persist via updatedPermissions
|
|
31
|
+
|
|
32
|
+
const BASH_PATTERNS = [
|
|
33
|
+
// Node tool invocations — covers $FORGE_ROOT/tools/*.cjs and $CLAUDE_PLUGIN_ROOT
|
|
34
|
+
{ pattern: /^node\s+.*\/tools\/[\w-]+\.(cjs|js)\b/, rule: 'node ~/.claude/plugins/cache/forge/forge/*/tools/*' },
|
|
35
|
+
// NOTE: node -e and node -p removed — arbitrary code execution must not be auto-approved.
|
|
36
|
+
// Forge workflows use node .../tools/*.cjs for tool invocations; inline node -e/p requires
|
|
37
|
+
// explicit user approval each time.
|
|
38
|
+
// Shell commands used by Forge workflows
|
|
39
|
+
{ pattern: /^mkdir\s+-p\s+/, rule: 'mkdir -p .forge/*' },
|
|
40
|
+
{ pattern: /^mkdir\s+-p\s+\S+/, rule: 'mkdir -p .forge/*' },
|
|
41
|
+
{ pattern: /^cp\s+/, rule: 'cp */schemas/*.schema.json .forge/schemas/' },
|
|
42
|
+
{ pattern: /^ls\s+/, rule: 'ls *' },
|
|
43
|
+
{ pattern: /^cat\s+/, rule: 'cat .forge/*' },
|
|
44
|
+
{ pattern: /^date\s+-u\s+/, rule: 'date -u *' },
|
|
45
|
+
{ pattern: /^date\s+/, rule: 'date -u *' },
|
|
46
|
+
{ pattern: /^jq\s+/, rule: 'jq *' },
|
|
47
|
+
{ pattern: /^touch\s+/, rule: 'touch .forge/*' },
|
|
48
|
+
{ pattern: /^uname\s+/, rule: 'uname *' },
|
|
49
|
+
{ pattern: /^rm\s+\.forge/, rule: 'rm .forge/*' },
|
|
50
|
+
{ pattern: /^rm\s+-rf\s+\.forge/, rule: 'rm -rf .forge/*' },
|
|
51
|
+
{ pattern: /^rmdir\s+/, rule: 'rmdir .forge/*' },
|
|
52
|
+
{ pattern: /^gh\s+auth\s+/, rule: 'gh auth status *' },
|
|
53
|
+
{ pattern: /^gh\s+issue\s+/, rule: 'gh issue create *' },
|
|
54
|
+
// git read-only commands (already auto-approved by Claude Code, but belt-and-suspenders)
|
|
55
|
+
{ pattern: /^git\s+status\b/, rule: 'git status *' },
|
|
56
|
+
{ pattern: /^git\s+log\b/, rule: 'git log *' },
|
|
57
|
+
{ pattern: /^git\s+diff\b/, rule: 'git diff *' },
|
|
58
|
+
{ pattern: /^git\s+add\s+/, rule: 'git add *' },
|
|
59
|
+
{ pattern: /^git\s+commit\s+-m\s+/, rule: 'git commit -m *' },
|
|
60
|
+
{ pattern: /^git\s+push\b/, rule: 'git push *' },
|
|
61
|
+
{ pattern: /^git\s+checkout\s+/, rule: 'git checkout *' },
|
|
62
|
+
{ pattern: /^git\s+branch\s+/, rule: 'git branch *' },
|
|
63
|
+
{ pattern: /^git\s+stash\b/, rule: 'git stash *' },
|
|
64
|
+
{ pattern: /^git\s+worktree\s+/, rule: 'git worktree *' },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const WRITE_PATTERNS = [
|
|
68
|
+
{ pattern: /^\.forge\//, rule: '.forge/**' },
|
|
69
|
+
{ pattern: /^\.claude\/commands\//, rule: '.claude/commands/**' },
|
|
70
|
+
{ pattern: /^engineering\//, rule: 'engineering/**' },
|
|
71
|
+
{ pattern: /^CLAUDE\.md$/i, rule: 'CLAUDE.md' },
|
|
72
|
+
{ pattern: /^AGENTS\.md$/i, rule: 'AGENTS.md' },
|
|
73
|
+
{ pattern: /^\.gitignore$/, rule: '.gitignore' },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const EDIT_PATTERNS = [
|
|
77
|
+
{ pattern: /^\.forge\//, rule: '.forge/**' },
|
|
78
|
+
{ pattern: /^\.claude\/commands\//, rule: '.claude/commands/**' },
|
|
79
|
+
{ pattern: /^engineering\//, rule: 'engineering/**' },
|
|
80
|
+
{ pattern: /^CLAUDE\.md$/i, rule: 'CLAUDE.md' },
|
|
81
|
+
{ pattern: /^AGENTS\.md$/i, rule: 'AGENTS.md' },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const WEBFETCH_PATTERNS = [
|
|
85
|
+
{ pattern: /^https:\/\/raw\.githubusercontent\.com\/Entelligentsia\/forge\//, rule: 'domain:raw.githubusercontent.com' },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const PATTERN_MAP = {
|
|
89
|
+
Bash: BASH_PATTERNS,
|
|
90
|
+
Write: WRITE_PATTERNS,
|
|
91
|
+
Edit: EDIT_PATTERNS,
|
|
92
|
+
MultiEdit: EDIT_PATTERNS,
|
|
93
|
+
WebFetch: WEBFETCH_PATTERNS,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// ── Core logic ─────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function matchTool(toolName, toolInput) {
|
|
99
|
+
const patterns = PATTERN_MAP[toolName];
|
|
100
|
+
if (!patterns) return null;
|
|
101
|
+
|
|
102
|
+
const input = toolName === 'Bash' ? (toolInput.command || '')
|
|
103
|
+
: (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') ? (toolInput.file_path || '')
|
|
104
|
+
: toolName === 'WebFetch' ? (toolInput.url || '')
|
|
105
|
+
: '';
|
|
106
|
+
|
|
107
|
+
for (const { pattern, rule } of patterns) {
|
|
108
|
+
if (pattern.test(input)) return rule;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Export for testing ─────────────────────────────────────────────
|
|
114
|
+
module.exports = { matchTool, BASH_PATTERNS, WRITE_PATTERNS, EDIT_PATTERNS, WEBFETCH_PATTERNS };
|
|
115
|
+
|
|
116
|
+
// ── Main (hook runner) ────────────────────────────────────────────
|
|
117
|
+
if (require.main === module) {
|
|
118
|
+
let input = '';
|
|
119
|
+
process.stdin.on('data', (d) => { input += d; });
|
|
120
|
+
process.stdin.on('end', () => {
|
|
121
|
+
let event;
|
|
122
|
+
try {
|
|
123
|
+
event = JSON.parse(input);
|
|
124
|
+
} catch (_) {
|
|
125
|
+
// Unparseable input — fail open, let normal permission flow handle it
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { tool_name, tool_input } = event;
|
|
130
|
+
if (!tool_name || !tool_input) {
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const matchedRule = matchTool(tool_name, tool_input || {});
|
|
135
|
+
if (matchedRule) {
|
|
136
|
+
// Persist only the matched rule — never bulk-approve all rules at once.
|
|
137
|
+
const response = {
|
|
138
|
+
hookSpecificOutput: {
|
|
139
|
+
hookEventName: 'PermissionRequest',
|
|
140
|
+
decision: {
|
|
141
|
+
behavior: 'allow',
|
|
142
|
+
updatedPermissions: [{
|
|
143
|
+
type: 'addRules',
|
|
144
|
+
rules: [{ toolName: tool_name, ruleContent: matchedRule }],
|
|
145
|
+
behavior: 'allow',
|
|
146
|
+
destination: 'localSettings',
|
|
147
|
+
}],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
process.stdout.write(JSON.stringify(response));
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Not a Forge pattern — exit 0 with no output to let normal permission flow proceed
|
|
156
|
+
process.exit(0);
|
|
157
|
+
});
|
|
158
|
+
} // end require.main === module
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Forge PostToolUse hook — error triage
|
|
3
|
+
//
|
|
4
|
+
// Fires after every Bash tool call. If the command is Forge-related and
|
|
5
|
+
// exits non-zero, injects an additionalContext prompt asking Claude to offer
|
|
6
|
+
// the user the option to file a bug via /forge:report-bug.
|
|
7
|
+
//
|
|
8
|
+
// Uses only Node.js built-ins — no npm dependencies required.
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const FORGE_PATTERNS = [
|
|
13
|
+
/manage-config/,
|
|
14
|
+
/\.forge\//,
|
|
15
|
+
/CLAUDE_PLUGIN_ROOT/,
|
|
16
|
+
/FORGE_ROOT/,
|
|
17
|
+
/MANAGE_CONFIG/,
|
|
18
|
+
/engineering\/tools\//,
|
|
19
|
+
/forge:init/,
|
|
20
|
+
/forge:health/,
|
|
21
|
+
/forge:regenerate/,
|
|
22
|
+
/forge:update/,
|
|
23
|
+
/forge:add-pipeline/,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function isForgeRelated(command) {
|
|
27
|
+
return FORGE_PATTERNS.some(p => p.test(command));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The hook must never exit non-zero — a hook failure would surface as noise to
|
|
31
|
+
// the user and defeat the purpose of silent triage. Wrap everything so that any
|
|
32
|
+
// unexpected error causes a clean no-op exit instead of a crash report.
|
|
33
|
+
process.on('uncaughtException', () => process.exit(0));
|
|
34
|
+
|
|
35
|
+
let raw = '';
|
|
36
|
+
process.stdin.setEncoding('utf8');
|
|
37
|
+
process.stdin.on('data', chunk => { raw += chunk; });
|
|
38
|
+
process.stdin.on('end', () => {
|
|
39
|
+
try {
|
|
40
|
+
const event = JSON.parse(raw);
|
|
41
|
+
|
|
42
|
+
if (event.tool_name !== 'Bash') return;
|
|
43
|
+
|
|
44
|
+
const command = event.tool_input?.command || '';
|
|
45
|
+
const exitCode = event.tool_response?.exitCode;
|
|
46
|
+
|
|
47
|
+
// Only act on non-zero exits for Forge-related commands.
|
|
48
|
+
if (!isForgeRelated(command)) return;
|
|
49
|
+
if (exitCode === 0 || exitCode === undefined) return;
|
|
50
|
+
|
|
51
|
+
const stderr = event.tool_response?.stderr || '';
|
|
52
|
+
const output = event.tool_response?.output || '';
|
|
53
|
+
const errorSnippet = (stderr || output).split('\n').slice(0, 3).join(' ').trim();
|
|
54
|
+
|
|
55
|
+
const context =
|
|
56
|
+
`FORGE_ERROR_TRIAGE: A Forge command just failed (exit ${exitCode}). ` +
|
|
57
|
+
(errorSnippet ? `First error line: "${errorSnippet}". ` : '') +
|
|
58
|
+
`Tell the user what went wrong, then ask: ` +
|
|
59
|
+
`"Would you like to file this as a Forge bug to help improve the tool? ` +
|
|
60
|
+
`Run /forge:report-bug and I will pre-fill the report from this conversation."`;
|
|
61
|
+
|
|
62
|
+
process.stdout.write(JSON.stringify({
|
|
63
|
+
hookSpecificOutput: {
|
|
64
|
+
hookEventName: 'PostToolUse',
|
|
65
|
+
additionalContext: context,
|
|
66
|
+
},
|
|
67
|
+
}) + '\n');
|
|
68
|
+
} catch {
|
|
69
|
+
// Swallow all errors — this hook must never become the problem.
|
|
70
|
+
}
|
|
71
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Forge write-boundary hook — runs PreToolUse on Write / Edit / MultiEdit.
|
|
3
|
+
//
|
|
4
|
+
// Purpose: enforce Forge schemas at the filesystem boundary so agents remain
|
|
5
|
+
// free to bypass deterministic tools (store-cli), as long as any write they
|
|
6
|
+
// do against Forge-owned paths honors the schema contract.
|
|
7
|
+
//
|
|
8
|
+
// Protocol (Claude Code PreToolUse hook):
|
|
9
|
+
// - stdin: JSON envelope { tool_name, tool_input, ... }
|
|
10
|
+
// - exit 0: allow the tool call
|
|
11
|
+
// - exit 2 with stderr payload: block the tool call and surface the message
|
|
12
|
+
//
|
|
13
|
+
// Fail-open philosophy: any internal error (unreadable schema, parse bug,
|
|
14
|
+
// unexpected tool input shape) exits 0 with a stderr warning. A broken
|
|
15
|
+
// validator must never block legitimate work — validate-store.cjs remains
|
|
16
|
+
// as a post-hoc auditor.
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
process.on('uncaughtException', (err) => {
|
|
21
|
+
try { process.stderr.write(`forge validate-write: internal error (fail-open): ${err.message}\n`); } catch (_) {}
|
|
22
|
+
process.exit(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
const { matchRegistry } = require('./lib/write-registry.js');
|
|
29
|
+
|
|
30
|
+
// Schema resolution order mirrors store-cli.cjs so the hook sees the exact
|
|
31
|
+
// same schemas as tool writes do.
|
|
32
|
+
const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || path.join(__dirname, '..');
|
|
33
|
+
|
|
34
|
+
function resolveValidator() {
|
|
35
|
+
// store-cli's shared validator lives at forge/tools/lib/validate.js. Require
|
|
36
|
+
// it relative to the plugin root so the hook works from both dev tree and
|
|
37
|
+
// installed plugin cache.
|
|
38
|
+
const candidates = [
|
|
39
|
+
path.join(PLUGIN_ROOT, 'tools', 'lib', 'validate.js'),
|
|
40
|
+
path.join(__dirname, '..', 'tools', 'lib', 'validate.js'),
|
|
41
|
+
];
|
|
42
|
+
for (const c of candidates) {
|
|
43
|
+
if (fs.existsSync(c)) return require(c);
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`validate.js not found (looked in: ${candidates.join(', ')})`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadSchema(filename) {
|
|
49
|
+
const candidates = [
|
|
50
|
+
path.join(process.cwd(), '.forge', 'schemas', filename),
|
|
51
|
+
path.join(process.cwd(), 'forge', 'schemas', filename),
|
|
52
|
+
path.join(PLUGIN_ROOT, 'schemas', filename),
|
|
53
|
+
path.join(__dirname, '..', 'schemas', filename),
|
|
54
|
+
];
|
|
55
|
+
for (const c of candidates) {
|
|
56
|
+
if (fs.existsSync(c)) {
|
|
57
|
+
return JSON.parse(fs.readFileSync(c, 'utf8'));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`schema not found: ${filename}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readStdinSync() {
|
|
64
|
+
try {
|
|
65
|
+
return fs.readFileSync(0, 'utf8');
|
|
66
|
+
} catch (_) {
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Apply Edit semantics: replace old_string with new_string in contents.
|
|
72
|
+
// If replace_all is true, replace every occurrence; otherwise replace the
|
|
73
|
+
// first (and only) occurrence. Errors if old_string is absent or ambiguous
|
|
74
|
+
// when replace_all is false — mirrors the Edit tool contract.
|
|
75
|
+
function applyEdit(contents, oldStr, newStr, replaceAll) {
|
|
76
|
+
if (oldStr === '' && contents === '') return newStr; // new-file Edit
|
|
77
|
+
if (oldStr === '') throw new Error('Edit: old_string is empty');
|
|
78
|
+
if (replaceAll) return contents.split(oldStr).join(newStr);
|
|
79
|
+
const idx = contents.indexOf(oldStr);
|
|
80
|
+
if (idx === -1) throw new Error('Edit: old_string not found in file');
|
|
81
|
+
const next = contents.indexOf(oldStr, idx + oldStr.length);
|
|
82
|
+
if (next !== -1) throw new Error('Edit: old_string is ambiguous (appears more than once)');
|
|
83
|
+
return contents.slice(0, idx) + newStr + contents.slice(idx + oldStr.length);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function computePostEditContents(toolName, toolInput) {
|
|
87
|
+
const filePath = toolInput.file_path;
|
|
88
|
+
if (toolName === 'Write') {
|
|
89
|
+
return { filePath, contents: toolInput.content != null ? String(toolInput.content) : '' };
|
|
90
|
+
}
|
|
91
|
+
const prior = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
92
|
+
if (toolName === 'Edit') {
|
|
93
|
+
return {
|
|
94
|
+
filePath,
|
|
95
|
+
contents: applyEdit(prior, toolInput.old_string || '', toolInput.new_string || '', !!toolInput.replace_all),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// MultiEdit
|
|
99
|
+
let cur = prior;
|
|
100
|
+
const edits = Array.isArray(toolInput.edits) ? toolInput.edits : [];
|
|
101
|
+
for (const e of edits) {
|
|
102
|
+
cur = applyEdit(cur, e.old_string || '', e.new_string || '', !!e.replace_all);
|
|
103
|
+
}
|
|
104
|
+
return { filePath, contents: cur, prior };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseProgressLine(line) {
|
|
108
|
+
// timestamp|agentName|bannerKey|status|detail
|
|
109
|
+
const parts = line.split('|');
|
|
110
|
+
if (parts.length < 4) return null;
|
|
111
|
+
return {
|
|
112
|
+
timestamp: parts[0],
|
|
113
|
+
agentName: parts[1],
|
|
114
|
+
bannerKey: parts[2],
|
|
115
|
+
status: parts[3],
|
|
116
|
+
detail: parts.slice(4).join('|'),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateProgressAppend(prior, proposed, validator, schema) {
|
|
121
|
+
// Only validate lines that are NEW — anything already on disk is grandfathered.
|
|
122
|
+
if (!proposed.startsWith(prior)) {
|
|
123
|
+
// Wholesale rewrite, not an append. Validate every non-empty line.
|
|
124
|
+
const lines = proposed.split('\n').filter(l => l.length > 0);
|
|
125
|
+
return lines.flatMap((l, i) => annotateLine(validator, schema, l, i));
|
|
126
|
+
}
|
|
127
|
+
const suffix = proposed.slice(prior.length);
|
|
128
|
+
const newLines = suffix.split('\n').filter(l => l.length > 0);
|
|
129
|
+
return newLines.flatMap((l, i) => annotateLine(validator, schema, l, i));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function annotateLine(validator, schema, line, idx) {
|
|
133
|
+
const rec = parseProgressLine(line);
|
|
134
|
+
if (!rec) return [`line ${idx + 1}: malformed (expected 4+ pipe-delimited fields)`];
|
|
135
|
+
const errors = validator.validateRecord(rec, schema);
|
|
136
|
+
return errors.map(e => `line ${idx + 1}: ${e}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function writeBypassAudit(filePath, reason) {
|
|
140
|
+
try {
|
|
141
|
+
const m = /\/\.forge\/store\/events\/([^/]+)\//.exec(filePath) || /\.forge\/store\/events\/([^/]+)\//.exec(filePath);
|
|
142
|
+
const bucket = m ? m[1] : 'unknown';
|
|
143
|
+
const logPath = path.join(process.cwd(), '.forge', 'store', 'events', bucket, 'progress.log');
|
|
144
|
+
if (!fs.existsSync(path.dirname(logPath))) return;
|
|
145
|
+
const ts = new Date().toISOString();
|
|
146
|
+
const line = `${ts}|forge-hook|write-boundary|progress|${reason}\n`;
|
|
147
|
+
fs.appendFileSync(logPath, line, 'utf8');
|
|
148
|
+
} catch (_) { /* audit best-effort */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function block(message) {
|
|
152
|
+
process.stderr.write(message + '\n');
|
|
153
|
+
process.exit(2);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function main() {
|
|
157
|
+
const raw = readStdinSync();
|
|
158
|
+
if (!raw) process.exit(0);
|
|
159
|
+
|
|
160
|
+
let envelope;
|
|
161
|
+
try { envelope = JSON.parse(raw); } catch (_) { process.exit(0); }
|
|
162
|
+
|
|
163
|
+
const toolName = envelope.tool_name;
|
|
164
|
+
if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) process.exit(0);
|
|
165
|
+
|
|
166
|
+
const toolInput = envelope.tool_input || {};
|
|
167
|
+
const filePath = toolInput.file_path;
|
|
168
|
+
if (!filePath || typeof filePath !== 'string') process.exit(0);
|
|
169
|
+
|
|
170
|
+
const entry = matchRegistry(filePath);
|
|
171
|
+
if (!entry) process.exit(0);
|
|
172
|
+
|
|
173
|
+
if (process.env.FORGE_SKIP_WRITE_VALIDATION === '1') {
|
|
174
|
+
writeBypassAudit(filePath, `FORGE_SKIP_WRITE_VALIDATION=1 bypass on ${toolName} ${path.relative(process.cwd(), filePath)}`);
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let validator, schema, post;
|
|
179
|
+
try {
|
|
180
|
+
validator = resolveValidator();
|
|
181
|
+
schema = loadSchema(entry.schema);
|
|
182
|
+
post = computePostEditContents(toolName, toolInput);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
process.stderr.write(`forge validate-write: setup error (fail-open): ${err.message}\n`);
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const relPath = path.relative(process.cwd(), post.filePath);
|
|
189
|
+
|
|
190
|
+
if (entry.format === 'line-pipe-delimited') {
|
|
191
|
+
const prior = post.prior != null ? post.prior : (fs.existsSync(post.filePath) ? fs.readFileSync(post.filePath, 'utf8') : '');
|
|
192
|
+
const errs = validateProgressAppend(prior, post.contents, validator, schema);
|
|
193
|
+
if (errs.length > 0) {
|
|
194
|
+
block(
|
|
195
|
+
'❌ Forge schema violation — write blocked\n' +
|
|
196
|
+
`Path: ${relPath}\n` +
|
|
197
|
+
`Kind: ${entry.kind}\n` +
|
|
198
|
+
`Violations:\n - ${errs.join('\n - ')}\n` +
|
|
199
|
+
`Hint: progress.log lines must be "timestamp|agentName|bannerKey|status|detail"; see forge/schemas/${entry.schema}.\n` +
|
|
200
|
+
'To bypass for one turn (emergency repair): FORGE_SKIP_WRITE_VALIDATION=1.'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// JSON payloads
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = JSON.parse(post.contents);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
block(
|
|
212
|
+
'❌ Forge schema violation — write blocked\n' +
|
|
213
|
+
`Path: ${relPath}\n` +
|
|
214
|
+
`Kind: ${entry.kind}\n` +
|
|
215
|
+
`Violation: Invalid JSON: ${err.message}\n` +
|
|
216
|
+
`Hint: see forge/schemas/${entry.schema} for the expected shape.\n` +
|
|
217
|
+
'To bypass for one turn (emergency repair): FORGE_SKIP_WRITE_VALIDATION=1.'
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const errs = validator.validateRecord(parsed, schema);
|
|
222
|
+
if (errs.length > 0) {
|
|
223
|
+
block(
|
|
224
|
+
'❌ Forge schema violation — write blocked\n' +
|
|
225
|
+
`Path: ${relPath}\n` +
|
|
226
|
+
`Kind: ${entry.kind}\n` +
|
|
227
|
+
`Violations:\n - ${errs.join('\n - ')}\n` +
|
|
228
|
+
`Hint: see forge/schemas/${entry.schema} for the full shape.\n` +
|
|
229
|
+
'To bypass for one turn (emergency repair): FORGE_SKIP_WRITE_VALIDATION=1.'
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
main();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.46.1",
|
|
3
|
+
"generated": "2026-05-22",
|
|
4
|
+
"note": "Tamper-evident only. Authoritative source: /forge:update from remote.",
|
|
5
|
+
"files": {
|
|
6
|
+
"commands/add-pipeline.md": "402c1deb6f19ad99561b48548ad89e3e1f625b827dce633eb84317556c0a1a4a",
|
|
7
|
+
"commands/add-task.md": "b2640c4b3c8c4fb9d4a82633d8c4d8fcd13bd63d4d6deb2c5c6a2008508de770",
|
|
8
|
+
"commands/ask.md": "df15f0aa68c93d65bfef05adb514c377bfc65617cafcd79534eb9b4149302657",
|
|
9
|
+
"commands/calibrate.md": "c764e735c9212eaf785d3d0afaf926f536fef099421b2b1bde7a4b1e3114d443",
|
|
10
|
+
"commands/config.md": "67251316b806d706bb77f063128e29995c6b4abaf554f4d5558bd97a1a6cfcfe",
|
|
11
|
+
"commands/enhance.md": "4bbfc05cf85c862c49883bc36f1272f87cd49c8cbe00f9196e76d542e6eaa720",
|
|
12
|
+
"commands/health.md": "cf8a59cfb403ba268922bc60abcdd9255a3b5690ae0175bb2819b3f6789debc0",
|
|
13
|
+
"commands/init.md": "3c0522294c692e8fbb75f925370caee66f54b23d7dd3a692bedcd69f95718b28",
|
|
14
|
+
"commands/materialize.md": "195292c9e98f50773c1b0c84f9bbc42438c4c8c989a84bd98092f6692799d6d6",
|
|
15
|
+
"commands/migrate.md": "b226f10a6f202bfd4a840314f34d503da34a077abbf1dff8d2b40f864474a76b",
|
|
16
|
+
"commands/quiz-agent.md": "ed261a5c8ac7cd3a1d7c8b372c9bf099bba9a4f9dc0a41cb5c80f22b57a133a1",
|
|
17
|
+
"commands/regenerate.md": "7c14f87b1bb178dd98ebdf0efafb86792c2b8b079bc5052d02a7e618165ec525",
|
|
18
|
+
"commands/remove.md": "de8802ee8ad5db4c4f3d0f526eb8735ab7de1a4b8ad307355d11dac5e1e04fc6",
|
|
19
|
+
"commands/report-bug.md": "af8a54bf8887b35e5c880898dd45783f6c2e80d3dc031d6479a6be613ac43053",
|
|
20
|
+
"commands/store-query.md": "28925bd257ceb6645254628abf0e76524481460382192ea00081b17310b88fed",
|
|
21
|
+
"commands/store-repair.md": "9317be65deb400953b8642b4d353d5583d3b032546c0b8f73d6c3a9b3445ebdd",
|
|
22
|
+
"commands/update-tools.md": "768c0d7ec07a17055c3d4b1b31370812f4292e03b1496723ba36f8caf596a609",
|
|
23
|
+
"commands/update.md": "6f7be76884888b0cbbe5e60a6926bbaa368f2ee3191f2be40adc8d263ad414aa",
|
|
24
|
+
"agents/store-query-validator.md": "f4c3573edcf6e28809515705362df611806a805c5269404fb17e31433cf3a81c",
|
|
25
|
+
"agents/tomoshibi.md": "0112604af0856235d7f028683dbfa3f4af63355cb2dd79d26592e983a6ecec8b",
|
|
26
|
+
"hooks/check-update.js": "75a70718d088b7c92ac0343908d07c2eb3bb334c427947490af1d775790859e1",
|
|
27
|
+
"hooks/forge-permissions.js": "d72fe4a7f010d363798680fa6afeb004f8747e2479ebdf653bc2200c9be073dc",
|
|
28
|
+
"hooks/triage-error.js": "b1e9c1c9a9f3e4868fff295f2caf87a2a72a817ee767af35f6334c88e45e16d4",
|
|
29
|
+
"hooks/validate-write.js": "bf17f740ae43d85a7f4b18bc08e2628534c3698ec5202cb51ebd3f0792375f24",
|
|
30
|
+
"tools/verify-integrity.cjs": "3ec3c970dd3d7c3001f8f373bcc40556803eadd2fc2afafb14f1c232cba4cc3f"
|
|
31
|
+
}
|
|
32
|
+
}
|