@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +324 -0
  2. package/README.md +2 -1
  3. package/dist/CHANGELOG-forge-plugin.md +210 -0
  4. package/dist/bin/forge.js +20 -1
  5. package/dist/bin/forge.js.map +1 -1
  6. package/dist/extensions/forgecli/ask-user-tool.js +32 -20
  7. package/dist/extensions/forgecli/ask-user-tool.js.map +1 -1
  8. package/dist/extensions/forgecli/config-layer.d.ts +15 -0
  9. package/dist/extensions/forgecli/config-layer.js +4 -1
  10. package/dist/extensions/forgecli/config-layer.js.map +1 -1
  11. package/dist/extensions/forgecli/config-writer.js +4 -1
  12. package/dist/extensions/forgecli/config-writer.js.map +1 -1
  13. package/dist/extensions/forgecli/enhance.js +1 -1
  14. package/dist/extensions/forgecli/enhance.js.map +1 -1
  15. package/dist/extensions/forgecli/fix-bug.js +31 -1
  16. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  17. package/dist/extensions/forgecli/forge-cli-schema.json +19 -0
  18. package/dist/extensions/forgecli/forge-tools.js +80 -0
  19. package/dist/extensions/forgecli/forge-tools.js.map +1 -1
  20. package/dist/extensions/forgecli/forge-update-command.js +24 -18
  21. package/dist/extensions/forgecli/forge-update-command.js.map +1 -1
  22. package/dist/extensions/forgecli/friction-emit.d.ts +97 -0
  23. package/dist/extensions/forgecli/friction-emit.js +246 -0
  24. package/dist/extensions/forgecli/friction-emit.js.map +1 -0
  25. package/dist/extensions/forgecli/health-check.d.ts +10 -0
  26. package/dist/extensions/forgecli/health-check.js +160 -8
  27. package/dist/extensions/forgecli/health-check.js.map +1 -1
  28. package/dist/extensions/forgecli/hook-dispatcher.js +24 -2
  29. package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
  30. package/dist/extensions/forgecli/hooks/write-guard.js +5 -1
  31. package/dist/extensions/forgecli/hooks/write-guard.js.map +1 -1
  32. package/dist/extensions/forgecli/index.js +29 -5
  33. package/dist/extensions/forgecli/index.js.map +1 -1
  34. package/dist/extensions/forgecli/lib/store-error-remediation.d.ts +65 -0
  35. package/dist/extensions/forgecli/lib/store-error-remediation.js +298 -0
  36. package/dist/extensions/forgecli/lib/store-error-remediation.js.map +1 -0
  37. package/dist/extensions/forgecli/regenerate.d.ts +22 -0
  38. package/dist/extensions/forgecli/regenerate.js +133 -3
  39. package/dist/extensions/forgecli/regenerate.js.map +1 -1
  40. package/dist/extensions/forgecli/run-sprint.js +16 -1
  41. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  42. package/dist/extensions/forgecli/run-task.js +30 -8
  43. package/dist/extensions/forgecli/run-task.js.map +1 -1
  44. package/dist/extensions/forgecli/skill-curation-flag.d.ts +21 -0
  45. package/dist/extensions/forgecli/skill-curation-flag.js +71 -0
  46. package/dist/extensions/forgecli/skill-curation-flag.js.map +1 -0
  47. package/dist/extensions/forgecli/skill-curator-subagent.d.ts +101 -0
  48. package/dist/extensions/forgecli/skill-curator-subagent.js +342 -0
  49. package/dist/extensions/forgecli/skill-curator-subagent.js.map +1 -0
  50. package/dist/extensions/forgecli/skill-retriever.d.ts +84 -0
  51. package/dist/extensions/forgecli/skill-retriever.js +246 -0
  52. package/dist/extensions/forgecli/skill-retriever.js.map +1 -0
  53. package/dist/extensions/forgecli/skill-usage-tracker.d.ts +91 -0
  54. package/dist/extensions/forgecli/skill-usage-tracker.js +224 -0
  55. package/dist/extensions/forgecli/skill-usage-tracker.js.map +1 -0
  56. package/dist/extensions/forgecli/store-resolver.d.ts +18 -0
  57. package/dist/extensions/forgecli/store-resolver.js +44 -4
  58. package/dist/extensions/forgecli/store-resolver.js.map +1 -1
  59. package/dist/extensions/forgecli/store-validator.d.ts +3 -0
  60. package/dist/extensions/forgecli/store-validator.js +4 -2
  61. package/dist/extensions/forgecli/store-validator.js.map +1 -1
  62. package/dist/forge-payload/.base-pack/personas/supervisor.md +9 -0
  63. package/dist/forge-payload/.base-pack/workflows/enhance.md +344 -18
  64. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  65. package/dist/forge-payload/.schemas/event.schema.json +20 -2
  66. package/dist/forge-payload/.schemas/migrations.json +112 -0
  67. package/dist/forge-payload/.schemas/proposal.schema.json +40 -0
  68. package/dist/forge-payload/agents/store-query-validator.md +103 -0
  69. package/dist/forge-payload/agents/tomoshibi.md +185 -0
  70. package/dist/forge-payload/commands/regenerate.md +109 -20
  71. package/dist/forge-payload/hooks/check-update.js +378 -0
  72. package/dist/forge-payload/hooks/forge-permissions.js +158 -0
  73. package/dist/forge-payload/hooks/triage-error.js +71 -0
  74. package/dist/forge-payload/hooks/validate-write.js +236 -0
  75. package/dist/forge-payload/integrity.json +32 -0
  76. package/dist/forge-payload/meta/workflows/meta-enhance.md +344 -18
  77. package/dist/forge-payload/schemas/structure-manifest.json +511 -0
  78. package/dist/forge-payload/tools/build-persona-pack.cjs +120 -11
  79. package/dist/forge-payload/tools/compression-gate.cjs +192 -0
  80. package/dist/forge-payload/tools/delete-candidate-detector.cjs +114 -0
  81. package/dist/forge-payload/tools/judge-proposal.cjs +177 -0
  82. package/dist/forge-payload/tools/manage-versions.cjs +132 -4
  83. package/dist/forge-payload/tools/queue-drain.cjs +152 -0
  84. package/dist/forge-payload/tools/replay-scoring.cjs +117 -0
  85. package/node_modules/@mariozechner/clipboard/package.json +2 -1
  86. package/node_modules/@mariozechner/clipboard-linux-x64-musl/README.md +3 -0
  87. package/node_modules/@mariozechner/clipboard-linux-x64-musl/clipboard.linux-x64-musl.node +0 -0
  88. package/node_modules/@mariozechner/clipboard-linux-x64-musl/package.json +25 -0
  89. 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
+ }