forge-workflow 0.0.4 → 0.0.6
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/dev.md +345 -340
- package/.claude/commands/plan.md +566 -521
- package/.claude/commands/premerge.md +186 -176
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +448 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +212 -164
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +90 -48
- package/.claude/commands/validate.md +288 -282
- package/.claude/commands/verify.md +269 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +121 -105
- package/.claude/scripts/greptile-resolve.sh +558 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +342 -337
- package/.cline/workflows/plan.md +563 -518
- package/.cline/workflows/premerge.md +183 -173
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +445 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +209 -161
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +87 -45
- package/.cline/workflows/validate.md +285 -279
- package/.cline/workflows/verify.md +266 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +345 -340
- package/.codex/skills/plan/SKILL.md +566 -521
- package/.codex/skills/premerge/SKILL.md +186 -176
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +448 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +212 -164
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +90 -48
- package/.codex/skills/validate/SKILL.md +288 -282
- package/.codex/skills/verify/SKILL.md +269 -221
- package/.cursor/commands/dev.md +342 -337
- package/.cursor/commands/plan.md +563 -518
- package/.cursor/commands/premerge.md +183 -173
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +445 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +209 -161
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +87 -45
- package/.cursor/commands/validate.md +285 -279
- package/.cursor/commands/verify.md +266 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +347 -342
- package/.github/prompts/plan.prompt.md +568 -523
- package/.github/prompts/premerge.prompt.md +188 -178
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +450 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +214 -166
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +92 -50
- package/.github/prompts/validate.prompt.md +290 -284
- package/.github/prompts/verify.prompt.md +271 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +346 -341
- package/.kilocode/workflows/plan.md +567 -522
- package/.kilocode/workflows/premerge.md +187 -177
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +449 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +213 -165
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +91 -49
- package/.kilocode/workflows/validate.md +289 -283
- package/.kilocode/workflows/verify.md +270 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +345 -340
- package/.opencode/commands/plan.md +566 -521
- package/.opencode/commands/premerge.md +186 -176
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +448 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +212 -164
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +90 -48
- package/.opencode/commands/validate.md +288 -282
- package/.opencode/commands/verify.md +269 -221
- package/.roo/commands/dev.md +346 -341
- package/.roo/commands/plan.md +567 -522
- package/.roo/commands/premerge.md +187 -177
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +449 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +213 -165
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +91 -49
- package/.roo/commands/validate.md +289 -283
- package/.roo/commands/verify.md +270 -222
- package/AGENTS.md +272 -175
- package/CLAUDE.md +110 -100
- package/README.md +429 -416
- package/bin/forge-cmd.js +317 -313
- package/bin/forge-preflight.js +322 -309
- package/bin/forge.js +4765 -4303
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -251
- package/docs/ENHANCED_ONBOARDING.md +612 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -631
- package/docs/TOOLCHAIN.md +653 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1056
- package/lefthook.yml +50 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/_registry.js +134 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/dev.js +571 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/sync.js +55 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +602 -602
- package/lib/commands/worktree.js +310 -0
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -191
- package/lib/detect-worktree.js +47 -47
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-hash.js +26 -26
- package/lib/freshness-token.js +148 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/reset.js +309 -0
- package/lib/setup-action-log.js +139 -139
- package/lib/setup-summary-renderer.js +106 -106
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -192
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/task-ownership.js +117 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -128
- package/scripts/beads-context.sh +426 -0
- package/scripts/beads-context.test.js +567 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +789 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +493 -0
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +268 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/pr-coordinator.sh +706 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +809 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +455 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +46 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- package/.cursor/hooks/state/continual-learning-index.json +0 -19
- package/.cursor/hooks/state/continual-learning.json +0 -8
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module check-agents
|
|
5
|
+
*
|
|
6
|
+
* CLI command that validates all agent configs are complete and in sync.
|
|
7
|
+
*
|
|
8
|
+
* 1. Runs syncCommands({ check: true }) to verify agent command files match canonical source
|
|
9
|
+
* 2. Reads lib/agents/*.plugin.json to verify each agent with capabilities.commands: true
|
|
10
|
+
* has its command directory populated with expected files
|
|
11
|
+
* 3. Reports: missing files, out-of-sync files, stale files
|
|
12
|
+
* 4. Exits 0 if all clean, exits non-zero if issues found
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node scripts/check-agents.js
|
|
16
|
+
*
|
|
17
|
+
* Exports:
|
|
18
|
+
* checkAgents(repoRoot) -> { errors: string[], warnings: string[] }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { syncCommands } = require('./sync-commands');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate all agent configs are complete and in sync.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} repoRoot - Absolute path to the repository root
|
|
29
|
+
* @returns {{ errors: string[], warnings: string[] }}
|
|
30
|
+
*/
|
|
31
|
+
function checkAgents(repoRoot) {
|
|
32
|
+
/** @type {string[]} */
|
|
33
|
+
const errors = [];
|
|
34
|
+
/** @type {string[]} */
|
|
35
|
+
const warnings = [];
|
|
36
|
+
|
|
37
|
+
// ---- 1. Run sync check ----
|
|
38
|
+
let syncResult;
|
|
39
|
+
try {
|
|
40
|
+
syncResult = syncCommands({ check: true, dryRun: false, repoRoot });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
errors.push(`Sync check failed: ${err.message}`);
|
|
43
|
+
return { errors, warnings };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (syncResult.empty) {
|
|
47
|
+
errors.push('No command files found in .claude/commands/ — cannot verify sync.');
|
|
48
|
+
return { errors, warnings };
|
|
49
|
+
} else {
|
|
50
|
+
if (syncResult.manifestMissing) {
|
|
51
|
+
warnings.push(
|
|
52
|
+
'Sync manifest (.forge/sync-manifest.json) not found — stale file detection skipped. ' +
|
|
53
|
+
'Run "node scripts/sync-commands.js" to generate it.'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (syncResult.outOfSync && syncResult.outOfSync.length > 0) {
|
|
58
|
+
for (const entry of syncResult.outOfSync) {
|
|
59
|
+
const relPath = path.join(entry.dir, entry.filename);
|
|
60
|
+
const exists = fs.existsSync(entry.filePath);
|
|
61
|
+
const status = exists ? 'modified' : 'missing';
|
|
62
|
+
errors.push(`Out of sync [${entry.agent}]: ${relPath} (${status})`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (syncResult.staleFiles && syncResult.staleFiles.length > 0) {
|
|
67
|
+
for (const filePath of syncResult.staleFiles) {
|
|
68
|
+
const relPath = path.relative(repoRoot, filePath);
|
|
69
|
+
errors.push(`Stale file (no longer generated, should be removed): ${relPath}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---- 2. Validate plugin catalog ----
|
|
75
|
+
const pluginDir = path.join(repoRoot, 'lib', 'agents');
|
|
76
|
+
/** @type {object[]} */
|
|
77
|
+
const plugins = [];
|
|
78
|
+
|
|
79
|
+
if (!fs.existsSync(pluginDir)) {
|
|
80
|
+
warnings.push('No plugin directory found at lib/agents/ — plugin catalog validation skipped.');
|
|
81
|
+
} else {
|
|
82
|
+
const pluginFiles = fs.readdirSync(pluginDir).filter((f) => f.endsWith('.plugin.json'));
|
|
83
|
+
|
|
84
|
+
if (pluginFiles.length === 0) {
|
|
85
|
+
warnings.push('No plugin.json files found in lib/agents/ — plugin catalog validation skipped.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const file of pluginFiles) {
|
|
89
|
+
try {
|
|
90
|
+
const plugin = JSON.parse(fs.readFileSync(path.join(pluginDir, file), 'utf8'));
|
|
91
|
+
plugins.push(plugin);
|
|
92
|
+
} catch (_err) {
|
|
93
|
+
errors.push(`Failed to parse plugin file: lib/agents/${file}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// For each plugin with commands: true, verify its command directory has files
|
|
98
|
+
for (const plugin of plugins) {
|
|
99
|
+
if (!plugin.capabilities || !plugin.capabilities.commands) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Find the command directory from the plugin's directories config
|
|
104
|
+
const dirs = plugin.directories || {};
|
|
105
|
+
// Plugins use different keys: commands, workflows, prompts, skills
|
|
106
|
+
const commandDir = dirs.commands || dirs.workflows || dirs.prompts;
|
|
107
|
+
|
|
108
|
+
if (!commandDir) {
|
|
109
|
+
// Plugin declares commands: true but has no command directory configured
|
|
110
|
+
// Codex is the only agent that uses skills as its command mechanism
|
|
111
|
+
if (plugin.id === 'codex' && dirs.skills) {
|
|
112
|
+
// Codex CLI: commands live in .codex/skills/<name>/SKILL.md — skip standard check
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
warnings.push(
|
|
116
|
+
`Plugin "${plugin.name}" (${plugin.id}) has commands: true but no command directory configured.`
|
|
117
|
+
);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const absCmdDir = path.join(repoRoot, commandDir);
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(absCmdDir)) {
|
|
124
|
+
errors.push(
|
|
125
|
+
`Plugin "${plugin.name}" (${plugin.id}) has commands: true but directory "${commandDir}" does not exist.`
|
|
126
|
+
);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check the directory has at least one file
|
|
131
|
+
const files = fs.readdirSync(absCmdDir).filter((f) => !f.startsWith('.'));
|
|
132
|
+
if (files.length === 0) {
|
|
133
|
+
errors.push(
|
|
134
|
+
`Plugin "${plugin.name}" (${plugin.id}) has commands: true but directory "${commandDir}" is empty.`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { errors, warnings };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---- CLI entry point ----
|
|
144
|
+
|
|
145
|
+
if (require.main === module) {
|
|
146
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
147
|
+
const result = checkAgents(repoRoot);
|
|
148
|
+
|
|
149
|
+
// Print warnings
|
|
150
|
+
for (const warning of result.warnings) {
|
|
151
|
+
console.warn(`Warning: ${warning}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Print errors
|
|
155
|
+
for (const error of result.errors) {
|
|
156
|
+
console.error(`Error: ${error}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Summary
|
|
160
|
+
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
161
|
+
console.log('All agent checks passed.');
|
|
162
|
+
process.exit(0);
|
|
163
|
+
} else if (result.errors.length === 0) {
|
|
164
|
+
console.log(`\nAll agent checks passed with ${result.warnings.length} warning(s).`);
|
|
165
|
+
process.exit(0);
|
|
166
|
+
} else {
|
|
167
|
+
console.error(`\n${result.errors.length} error(s), ${result.warnings.length} warning(s) found.`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { checkAgents };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const TOKEN_FILENAME = '.forge-push-token';
|
|
8
|
+
const MAX_AGE_MS = 30000; // 30 seconds
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the absolute path to the token file.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
14
|
+
* @returns {string} Token file path
|
|
15
|
+
*/
|
|
16
|
+
function tokenPath(projectRoot) {
|
|
17
|
+
return path.join(projectRoot, TOKEN_FILENAME);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Write a one-time nonce token to disk.
|
|
22
|
+
* Called by `forge push` right before `git push` so lefthook hooks
|
|
23
|
+
* can detect that checks already passed and skip re-running them.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
26
|
+
* @returns {{nonce: string, timestamp: number}} The written token data
|
|
27
|
+
*/
|
|
28
|
+
function write(projectRoot) {
|
|
29
|
+
const data = {
|
|
30
|
+
nonce: crypto.randomUUID(),
|
|
31
|
+
timestamp: Date.now(),
|
|
32
|
+
};
|
|
33
|
+
fs.writeFileSync(tokenPath(projectRoot), JSON.stringify(data), 'utf-8');
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if a valid (fresh, well-formed) forge push token exists.
|
|
39
|
+
* Does NOT delete the token — use `consume()` for one-time validation.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
42
|
+
* @returns {boolean} True if a valid, fresh token exists
|
|
43
|
+
*/
|
|
44
|
+
function isValid(projectRoot) {
|
|
45
|
+
try {
|
|
46
|
+
const raw = fs.readFileSync(tokenPath(projectRoot), 'utf-8');
|
|
47
|
+
const content = JSON.parse(raw);
|
|
48
|
+
if (!content.nonce || typeof content.timestamp !== 'number') {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const age = Date.now() - content.timestamp;
|
|
52
|
+
return age < MAX_AGE_MS;
|
|
53
|
+
} catch (_err) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate and delete the token in one atomic operation (one-time use).
|
|
60
|
+
* Always attempts to delete the file, even if the token is stale or invalid.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
63
|
+
* @returns {boolean} True if the token was valid before deletion
|
|
64
|
+
*/
|
|
65
|
+
function consume(projectRoot) {
|
|
66
|
+
const tp = tokenPath(projectRoot);
|
|
67
|
+
try {
|
|
68
|
+
const raw = fs.readFileSync(tp, 'utf-8');
|
|
69
|
+
// Always delete after reading — whether valid or not
|
|
70
|
+
try { fs.unlinkSync(tp); } catch (_e) { /* already gone */ }
|
|
71
|
+
|
|
72
|
+
const content = JSON.parse(raw);
|
|
73
|
+
if (!content.nonce || typeof content.timestamp !== 'number') {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const age = Date.now() - content.timestamp;
|
|
77
|
+
return age < MAX_AGE_MS;
|
|
78
|
+
} catch (_err) {
|
|
79
|
+
// File doesn't exist or isn't readable — try cleanup anyway
|
|
80
|
+
try { fs.unlinkSync(tp); } catch (_e) { /* nothing to clean */ }
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { write, isValid, consume };
|
|
86
|
+
|
|
87
|
+
// When run directly as a script (e.g., from lefthook skip check):
|
|
88
|
+
// Exit 0 = token valid (skip hooks), Exit 1 = run hooks normally.
|
|
89
|
+
// Uses isValid() (non-destructive) so multiple lefthook commands can
|
|
90
|
+
// each check the same token. The token expires after 30s automatically.
|
|
91
|
+
if (require.main === module) {
|
|
92
|
+
const projectRoot = process.cwd();
|
|
93
|
+
if (isValid(projectRoot)) {
|
|
94
|
+
console.log('forge push token valid — skipping pre-push hooks');
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cross-platform commitlint runner for lefthook commit-msg hook.
|
|
4
|
+
* Replaces direct npx invocation which fails on Windows without shell: true.
|
|
5
|
+
* Works on Windows CMD, PowerShell, macOS, Linux.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
// On Windows, npx is npx.cmd — shell: true resolves .cmd extensions
|
|
11
|
+
const isWindows = process.platform === 'win32';
|
|
12
|
+
|
|
13
|
+
const commitMsgFile = process.argv[2];
|
|
14
|
+
if (!commitMsgFile) {
|
|
15
|
+
console.error('❌ No commit message file provided');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Detect package manager: prefer bun (fast, works with bun.lock), fall back to npx
|
|
20
|
+
// npx fails on Windows when npm 11's arborist encounters bun.lock instead of package-lock.json
|
|
21
|
+
const hasBunLock = require('node:fs').existsSync(
|
|
22
|
+
require('node:path').join(__dirname, '../bun.lock')
|
|
23
|
+
);
|
|
24
|
+
const runner = hasBunLock ? 'bunx' : 'npx';
|
|
25
|
+
|
|
26
|
+
const result = spawnSync(
|
|
27
|
+
runner,
|
|
28
|
+
['commitlint', '--edit', commitMsgFile],
|
|
29
|
+
{ stdio: 'inherit', shell: isWindows }
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (result.error) {
|
|
33
|
+
console.error('');
|
|
34
|
+
console.error(`❌ Failed to run commitlint: ${result.error.message}`);
|
|
35
|
+
console.error(' Is Node.js/npm installed and on PATH?');
|
|
36
|
+
console.error('');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (result.status !== 0) {
|
|
41
|
+
process.exit(result.status);
|
|
42
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# conflict-detect.sh — Detect file/module overlaps between in-progress issues.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# conflict-detect.sh --issue <id> Check overlaps for a given issue
|
|
6
|
+
# conflict-detect.sh --files <file1,file2,...> Check arbitrary files against index
|
|
7
|
+
# conflict-detect.sh --issue <id> --detail Drill down to file-level overlap
|
|
8
|
+
# conflict-detect.sh --files <f1,f2> --detail Drill down to file-level overlap
|
|
9
|
+
#
|
|
10
|
+
# Exit codes:
|
|
11
|
+
# 0 = no conflicts found
|
|
12
|
+
# 1 = conflicts found
|
|
13
|
+
# 2 = usage/validation error (malformed input)
|
|
14
|
+
#
|
|
15
|
+
# OWASP A03: All inputs validated; shell variables quoted; jq --arg used.
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
+
|
|
21
|
+
# Source file-index helpers (file_index_read, file_index_get)
|
|
22
|
+
source "$SCRIPT_DIR/file-index.sh" || { echo "FATAL: failed to source file-index.sh" >&2; exit 2; }
|
|
23
|
+
|
|
24
|
+
# Source sync-utils (get_session_identity, sanitize_config_value)
|
|
25
|
+
# Only source if available (tests may not need all utils)
|
|
26
|
+
if [[ -f "$SCRIPT_DIR/sync-utils.sh" ]]; then
|
|
27
|
+
source "$SCRIPT_DIR/sync-utils.sh" || { echo "FATAL: failed to source sync-utils.sh" >&2; exit 2; }
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# ── Input Validation ─────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
# Validate issue_id: alphanumeric + hyphens only
|
|
33
|
+
_validate_issue_id_arg() {
|
|
34
|
+
local id="$1"
|
|
35
|
+
if [[ ! "$id" =~ ^[a-zA-Z0-9-]+$ ]]; then
|
|
36
|
+
echo "Error: invalid issue_id format: must be alphanumeric + hyphens only" >&2
|
|
37
|
+
return 1
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Validate a file path: no shell injection patterns
|
|
42
|
+
# Allows: alphanumeric, dots, hyphens, underscores, forward slashes
|
|
43
|
+
_validate_file_path() {
|
|
44
|
+
local path="$1"
|
|
45
|
+
if [[ ! "$path" =~ ^[a-zA-Z0-9./_@[:space:]-]+$ ]]; then
|
|
46
|
+
echo "Error: invalid file path — contains disallowed characters: $path" >&2
|
|
47
|
+
return 1
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# ── Stale Sync Check ─────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
# Check if .beads/.last-sync is older than 15 minutes (900 seconds).
|
|
54
|
+
# Prints a warning to stderr if stale.
|
|
55
|
+
_check_stale_sync() {
|
|
56
|
+
local root="${FILE_INDEX_ROOT:-.}"
|
|
57
|
+
local last_sync_file="$root/.beads/.last-sync"
|
|
58
|
+
|
|
59
|
+
if [[ ! -f "$last_sync_file" ]]; then
|
|
60
|
+
# No sync file — skip warning (first run)
|
|
61
|
+
return 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
local last_sync_ts
|
|
65
|
+
last_sync_ts="$(cat "$last_sync_file" 2>/dev/null | tr -d '[:space:]')"
|
|
66
|
+
|
|
67
|
+
if [[ -z "$last_sync_ts" ]]; then
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# .last-sync stores Unix epoch seconds (written by sync-utils.sh)
|
|
72
|
+
local last_epoch now_epoch
|
|
73
|
+
if [[ "$last_sync_ts" =~ ^[0-9]+$ ]]; then
|
|
74
|
+
last_epoch="$last_sync_ts"
|
|
75
|
+
else
|
|
76
|
+
# Fallback: try parsing as ISO 8601 (GNU date, then BSD date)
|
|
77
|
+
last_epoch="$(date -d "$last_sync_ts" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%SZ" "$last_sync_ts" +%s 2>/dev/null || echo 0)"
|
|
78
|
+
fi
|
|
79
|
+
now_epoch="$(date +%s)"
|
|
80
|
+
|
|
81
|
+
local diff=$(( now_epoch - last_epoch ))
|
|
82
|
+
|
|
83
|
+
if [[ "$diff" -gt 900 ]]; then
|
|
84
|
+
echo "WARNING: File index may be stale (last sync: ${last_sync_ts}, ${diff}s ago > 900s threshold)" >&2
|
|
85
|
+
fi
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# ── Module Extraction ────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
# Given a file path, derive its module directory (parent directory + /).
|
|
91
|
+
# e.g., "src/lib/status.ts" -> "src/lib/"
|
|
92
|
+
_derive_module() {
|
|
93
|
+
local filepath="$1"
|
|
94
|
+
local dir
|
|
95
|
+
dir="$(dirname "$filepath")"
|
|
96
|
+
# Ensure trailing slash
|
|
97
|
+
if [[ "$dir" == "." ]]; then
|
|
98
|
+
printf '%s' "./"
|
|
99
|
+
else
|
|
100
|
+
printf '%s/' "$dir"
|
|
101
|
+
fi
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# ── Core Conflict Detection ──────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
# Check for conflicts between a set of modules/files and all entries in the index.
|
|
107
|
+
# Arguments:
|
|
108
|
+
# $1 — JSON array of modules to check
|
|
109
|
+
# $2 — JSON array of files to check
|
|
110
|
+
# $3 — issue_id to exclude from comparison (empty string for --files mode)
|
|
111
|
+
# $4 — "true" or "false" for detail mode
|
|
112
|
+
# Outputs conflict report to stdout.
|
|
113
|
+
# Returns 0 if no conflicts, 1 if conflicts found.
|
|
114
|
+
_detect_conflicts() {
|
|
115
|
+
local check_modules="$1"
|
|
116
|
+
local check_files="$2"
|
|
117
|
+
local exclude_issue="$3"
|
|
118
|
+
local detail="$4"
|
|
119
|
+
|
|
120
|
+
# Read all active entries from file index, filter out stale entries (>48 hours)
|
|
121
|
+
local all_entries
|
|
122
|
+
local now_epoch
|
|
123
|
+
now_epoch="$(date +%s)"
|
|
124
|
+
all_entries="$(file_index_read 2>/dev/null | jq -c --arg now "$now_epoch" '
|
|
125
|
+
[.[] | select(
|
|
126
|
+
if .updated_at then
|
|
127
|
+
((.updated_at[:19] + "Z") | fromdateiso8601) as $ts |
|
|
128
|
+
(($now | tonumber) - $ts) < 172800
|
|
129
|
+
else true end
|
|
130
|
+
)]
|
|
131
|
+
' 2>/dev/null)" || all_entries="[]"
|
|
132
|
+
|
|
133
|
+
if [[ "$all_entries" == "[]" ]] || [[ -z "$all_entries" ]]; then
|
|
134
|
+
echo "No conflicts found. File index is empty."
|
|
135
|
+
return 0
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
local conflicts_found=0
|
|
139
|
+
local output=""
|
|
140
|
+
|
|
141
|
+
# Iterate over each entry in the index
|
|
142
|
+
local entry_count
|
|
143
|
+
entry_count="$(printf '%s' "$all_entries" | jq '. | length')"
|
|
144
|
+
|
|
145
|
+
local i=0
|
|
146
|
+
while [[ "$i" -lt "$entry_count" ]]; do
|
|
147
|
+
local entry
|
|
148
|
+
entry="$(printf '%s' "$all_entries" | jq -c ".[$i]")"
|
|
149
|
+
|
|
150
|
+
local entry_issue_id
|
|
151
|
+
entry_issue_id="$(printf '%s' "$entry" | jq -r '.issue_id')"
|
|
152
|
+
|
|
153
|
+
# Skip the issue we're checking (don't conflict with yourself)
|
|
154
|
+
if [[ -n "$exclude_issue" ]] && [[ "$entry_issue_id" == "$exclude_issue" ]]; then
|
|
155
|
+
i=$((i + 1))
|
|
156
|
+
continue
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
local entry_developer
|
|
160
|
+
entry_developer="$(printf '%s' "$entry" | jq -r '.developer')"
|
|
161
|
+
|
|
162
|
+
local entry_modules
|
|
163
|
+
entry_modules="$(printf '%s' "$entry" | jq -c '.modules')"
|
|
164
|
+
|
|
165
|
+
local entry_files
|
|
166
|
+
entry_files="$(printf '%s' "$entry" | jq -c '.files')"
|
|
167
|
+
|
|
168
|
+
# Check module overlap
|
|
169
|
+
local module_overlaps
|
|
170
|
+
module_overlaps="$(jq -n -c \
|
|
171
|
+
--argjson a "$check_modules" \
|
|
172
|
+
--argjson b "$entry_modules" \
|
|
173
|
+
'[$a[] as $m | $b[] | select(. == $m)] | unique')"
|
|
174
|
+
|
|
175
|
+
local module_overlap_count
|
|
176
|
+
module_overlap_count="$(printf '%s' "$module_overlaps" | jq '. | length')"
|
|
177
|
+
|
|
178
|
+
if [[ "$module_overlap_count" -gt 0 ]]; then
|
|
179
|
+
conflicts_found=1
|
|
180
|
+
local overlapping_modules_str
|
|
181
|
+
overlapping_modules_str="$(printf '%s' "$module_overlaps" | jq -r '.[]' | tr '\n' ', ' | sed 's/,$//')"
|
|
182
|
+
|
|
183
|
+
output+="CONFLICT: Issue ${entry_issue_id} (developer: ${entry_developer})"$'\n'
|
|
184
|
+
output+=" Overlapping modules: ${overlapping_modules_str}"$'\n'
|
|
185
|
+
|
|
186
|
+
# Detail mode: show file-level overlap
|
|
187
|
+
if [[ "$detail" == "true" ]]; then
|
|
188
|
+
local file_overlaps
|
|
189
|
+
file_overlaps="$(jq -n -c \
|
|
190
|
+
--argjson a "$check_files" \
|
|
191
|
+
--argjson b "$entry_files" \
|
|
192
|
+
'[$a[] as $f | $b[] | select(. == $f)] | unique')"
|
|
193
|
+
|
|
194
|
+
local file_overlap_count
|
|
195
|
+
file_overlap_count="$(printf '%s' "$file_overlaps" | jq '. | length')"
|
|
196
|
+
|
|
197
|
+
if [[ "$file_overlap_count" -gt 0 ]]; then
|
|
198
|
+
local overlapping_files_str
|
|
199
|
+
overlapping_files_str="$(printf '%s' "$file_overlaps" | jq -r '.[]' | tr '\n' ', ' | sed 's/,$//')"
|
|
200
|
+
output+=" Overlapping files: ${overlapping_files_str}"$'\n'
|
|
201
|
+
else
|
|
202
|
+
output+=" No exact file overlaps (module-level only)"$'\n'
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
i=$((i + 1))
|
|
208
|
+
done
|
|
209
|
+
|
|
210
|
+
if [[ "$conflicts_found" -eq 1 ]]; then
|
|
211
|
+
printf '%s' "$output"
|
|
212
|
+
return 1
|
|
213
|
+
else
|
|
214
|
+
echo "No conflicts found."
|
|
215
|
+
return 0
|
|
216
|
+
fi
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# ── Main ─────────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
main() {
|
|
222
|
+
local mode="" # "issue" or "files"
|
|
223
|
+
local issue_id=""
|
|
224
|
+
local files_arg=""
|
|
225
|
+
local detail="false"
|
|
226
|
+
|
|
227
|
+
# Parse arguments
|
|
228
|
+
while [[ $# -gt 0 ]]; do
|
|
229
|
+
case "$1" in
|
|
230
|
+
--issue)
|
|
231
|
+
mode="issue"
|
|
232
|
+
shift
|
|
233
|
+
if [[ $# -eq 0 ]]; then
|
|
234
|
+
echo "Error: --issue requires an argument" >&2
|
|
235
|
+
exit 1
|
|
236
|
+
fi
|
|
237
|
+
issue_id="$1"
|
|
238
|
+
shift
|
|
239
|
+
;;
|
|
240
|
+
--files)
|
|
241
|
+
mode="files"
|
|
242
|
+
shift
|
|
243
|
+
if [[ $# -eq 0 ]]; then
|
|
244
|
+
echo "Error: --files requires an argument" >&2
|
|
245
|
+
exit 1
|
|
246
|
+
fi
|
|
247
|
+
files_arg="$1"
|
|
248
|
+
shift
|
|
249
|
+
;;
|
|
250
|
+
--detail)
|
|
251
|
+
detail="true"
|
|
252
|
+
shift
|
|
253
|
+
;;
|
|
254
|
+
*)
|
|
255
|
+
echo "Error: unknown argument '$1'" >&2
|
|
256
|
+
echo "Usage: conflict-detect.sh --issue <id> [--detail]" >&2
|
|
257
|
+
echo " conflict-detect.sh --files <file1,file2,...> [--detail]" >&2
|
|
258
|
+
exit 1
|
|
259
|
+
;;
|
|
260
|
+
esac
|
|
261
|
+
done
|
|
262
|
+
|
|
263
|
+
# Require at least one mode
|
|
264
|
+
if [[ -z "$mode" ]]; then
|
|
265
|
+
echo "Usage: conflict-detect.sh --issue <id> [--detail]" >&2
|
|
266
|
+
echo " conflict-detect.sh --files <file1,file2,...> [--detail]" >&2
|
|
267
|
+
exit 1
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
# Check for stale sync
|
|
271
|
+
_check_stale_sync
|
|
272
|
+
|
|
273
|
+
if [[ "$mode" == "issue" ]]; then
|
|
274
|
+
# Validate issue_id
|
|
275
|
+
_validate_issue_id_arg "$issue_id" || exit 2
|
|
276
|
+
|
|
277
|
+
# Get the issue's entry from the file index
|
|
278
|
+
local entry
|
|
279
|
+
entry="$(file_index_get "$issue_id")"
|
|
280
|
+
|
|
281
|
+
if [[ "$entry" == "null" ]]; then
|
|
282
|
+
echo "Info: issue '$issue_id' not yet in file index — no conflicts possible." >&2
|
|
283
|
+
exit 0
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
local check_modules check_files
|
|
287
|
+
check_modules="$(printf '%s' "$entry" | jq -c '.modules')"
|
|
288
|
+
check_files="$(printf '%s' "$entry" | jq -c '.files')"
|
|
289
|
+
|
|
290
|
+
_detect_conflicts "$check_modules" "$check_files" "$issue_id" "$detail"
|
|
291
|
+
|
|
292
|
+
elif [[ "$mode" == "files" ]]; then
|
|
293
|
+
# Parse comma-separated file list
|
|
294
|
+
local IFS=','
|
|
295
|
+
local -a file_list
|
|
296
|
+
read -ra file_list <<< "$files_arg"
|
|
297
|
+
|
|
298
|
+
# Validate each file path
|
|
299
|
+
local -a valid_files=()
|
|
300
|
+
local -a derived_modules=()
|
|
301
|
+
for f in "${file_list[@]}"; do
|
|
302
|
+
# Trim whitespace
|
|
303
|
+
f="$(printf '%s' "$f" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
304
|
+
_validate_file_path "$f" || exit 2
|
|
305
|
+
valid_files+=("$f")
|
|
306
|
+
|
|
307
|
+
# Derive module from file path
|
|
308
|
+
local mod
|
|
309
|
+
mod="$(_derive_module "$f")"
|
|
310
|
+
derived_modules+=("$mod")
|
|
311
|
+
done
|
|
312
|
+
|
|
313
|
+
# Build JSON arrays using jq
|
|
314
|
+
local files_json modules_json
|
|
315
|
+
files_json="$(printf '%s\n' "${valid_files[@]}" | jq -R '.' | jq -s -c '.')"
|
|
316
|
+
# Deduplicate modules
|
|
317
|
+
modules_json="$(printf '%s\n' "${derived_modules[@]}" | jq -R '.' | jq -s -c 'unique')"
|
|
318
|
+
|
|
319
|
+
_detect_conflicts "$modules_json" "$files_json" "" "$detail"
|
|
320
|
+
fi
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
main "$@"
|