forge-workflow 0.0.4 → 0.0.5

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 (209) hide show
  1. package/.claude/commands/dev.md +340 -340
  2. package/.claude/commands/plan.md +521 -521
  3. package/.claude/commands/premerge.md +176 -176
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +442 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +164 -164
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -48
  10. package/.claude/commands/validate.md +282 -282
  11. package/.claude/commands/verify.md +221 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +105 -105
  14. package/.claude/scripts/greptile-resolve.sh +526 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +337 -337
  17. package/.cline/workflows/plan.md +518 -518
  18. package/.cline/workflows/premerge.md +173 -173
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +439 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +161 -161
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -45
  25. package/.cline/workflows/validate.md +279 -279
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -340
  29. package/.codex/skills/plan/SKILL.md +521 -521
  30. package/.codex/skills/premerge/SKILL.md +176 -176
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +442 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +164 -164
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -48
  37. package/.codex/skills/validate/SKILL.md +282 -282
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -337
  40. package/.cursor/commands/plan.md +518 -518
  41. package/.cursor/commands/premerge.md +173 -173
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +439 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +161 -161
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -45
  48. package/.cursor/commands/validate.md +279 -279
  49. package/.cursor/commands/verify.md +218 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +342 -342
  54. package/.github/prompts/plan.prompt.md +523 -523
  55. package/.github/prompts/premerge.prompt.md +178 -178
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +444 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +166 -166
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -50
  62. package/.github/prompts/validate.prompt.md +284 -284
  63. package/.github/prompts/verify.prompt.md +223 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +341 -341
  67. package/.kilocode/workflows/plan.md +522 -522
  68. package/.kilocode/workflows/premerge.md +177 -177
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +443 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +165 -165
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -49
  75. package/.kilocode/workflows/validate.md +283 -283
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -340
  79. package/.opencode/commands/plan.md +521 -521
  80. package/.opencode/commands/premerge.md +176 -176
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +442 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +164 -164
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -48
  87. package/.opencode/commands/validate.md +282 -282
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -341
  90. package/.roo/commands/plan.md +522 -522
  91. package/.roo/commands/premerge.md +177 -177
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +443 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +165 -165
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -49
  98. package/.roo/commands/validate.md +283 -283
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -175
  101. package/CLAUDE.md +100 -100
  102. package/README.md +429 -416
  103. package/bin/forge-cmd.js +313 -313
  104. package/bin/forge-preflight.js +309 -309
  105. package/bin/forge.js +4596 -4303
  106. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  107. package/docs/BEADS_GITHUB_SYNC.md +251 -251
  108. package/docs/ENHANCED_ONBOARDING.md +602 -602
  109. package/docs/EXAMPLES.md +482 -482
  110. package/docs/GREPTILE_SETUP.md +400 -400
  111. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  112. package/docs/ROADMAP.md +359 -359
  113. package/docs/SETUP.md +663 -631
  114. package/docs/TOOLCHAIN.md +630 -630
  115. package/docs/VALIDATION.md +363 -363
  116. package/install.sh +40 -1056
  117. package/lefthook.yml +39 -39
  118. package/lib/agents/README.md +198 -198
  119. package/lib/agents/claude.plugin.json +28 -28
  120. package/lib/agents/cline.plugin.json +22 -22
  121. package/lib/agents/codex.plugin.json +19 -19
  122. package/lib/agents/copilot.plugin.json +24 -24
  123. package/lib/agents/cursor.plugin.json +25 -25
  124. package/lib/agents/kilocode.plugin.json +22 -22
  125. package/lib/agents/opencode.plugin.json +20 -20
  126. package/lib/agents/roo.plugin.json +23 -23
  127. package/lib/agents-config.js +2112 -2112
  128. package/lib/beads-health-check.js +143 -0
  129. package/lib/beads-setup.js +341 -0
  130. package/lib/beads-sync-scaffold.js +260 -0
  131. package/lib/commands/dev.js +513 -513
  132. package/lib/commands/plan.js +692 -692
  133. package/lib/commands/recommend.js +119 -119
  134. package/lib/commands/ship.js +377 -377
  135. package/lib/commands/status.js +378 -378
  136. package/lib/commands/validate.js +602 -602
  137. package/lib/context-merge.js +359 -359
  138. package/lib/dep-guard/analyzer.js +294 -294
  139. package/lib/dep-guard/behavior-detector.js +98 -98
  140. package/lib/dep-guard/contract-detector.js +162 -162
  141. package/lib/dep-guard/import-detector.js +498 -498
  142. package/lib/dep-guard/path-utils.js +13 -13
  143. package/lib/dep-guard/rubric.js +120 -120
  144. package/lib/dep-guard/task-parser.js +318 -318
  145. package/lib/detect-agent.js +191 -191
  146. package/lib/detect-worktree.js +47 -47
  147. package/lib/file-hash.js +26 -26
  148. package/lib/husky-migration.js +450 -0
  149. package/lib/lefthook-check.js +65 -0
  150. package/lib/pat-setup.js +207 -0
  151. package/lib/plugin-catalog.js +350 -350
  152. package/lib/plugin-manager.js +166 -166
  153. package/lib/plugin-recommender.js +141 -141
  154. package/lib/project-discovery.js +491 -491
  155. package/lib/setup-action-log.js +139 -139
  156. package/lib/setup-summary-renderer.js +106 -106
  157. package/lib/setup-utils.js +96 -0
  158. package/lib/setup.js +192 -192
  159. package/lib/smart-merge.js +64 -0
  160. package/lib/symlink-utils.js +81 -0
  161. package/lib/workflow-profiles.js +197 -197
  162. package/package.json +131 -128
  163. package/scripts/beads-context.sh +291 -0
  164. package/scripts/beads-context.test.js +563 -0
  165. package/scripts/behavioral-judge.sh +378 -0
  166. package/scripts/benchmark.js +85 -0
  167. package/scripts/branch-protection.js +183 -0
  168. package/scripts/check-agents.js +172 -0
  169. package/scripts/commitlint.js +42 -0
  170. package/scripts/conflict-detect.sh +323 -0
  171. package/scripts/dep-guard-analyze.js +71 -0
  172. package/scripts/dep-guard.sh +811 -0
  173. package/scripts/eval_win.py +249 -0
  174. package/scripts/file-index.sh +399 -0
  175. package/scripts/github-beads-sync/comment.mjs +64 -0
  176. package/scripts/github-beads-sync/config.mjs +148 -0
  177. package/scripts/github-beads-sync/github-api.mjs +131 -0
  178. package/scripts/github-beads-sync/index.mjs +332 -0
  179. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  180. package/scripts/github-beads-sync/mapping.mjs +78 -0
  181. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  182. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  183. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  184. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  185. package/scripts/github-beads-sync.config.json +26 -0
  186. package/scripts/improve-command.js +375 -0
  187. package/scripts/lib/eval-runner.js +229 -0
  188. package/scripts/lib/eval-schema.js +135 -0
  189. package/scripts/lib/eval-storage.js +78 -0
  190. package/scripts/lib/grading.js +203 -0
  191. package/scripts/lib/transcript-parser.js +63 -0
  192. package/scripts/lint.js +47 -0
  193. package/scripts/migrate-to-bun-test.js +412 -0
  194. package/scripts/run-command-eval.js +236 -0
  195. package/scripts/smart-status.sh +782 -0
  196. package/scripts/sync-commands.js +571 -0
  197. package/scripts/sync-utils.sh +460 -0
  198. package/scripts/test-dashboard.js +123 -0
  199. package/scripts/test.js +44 -0
  200. package/scripts/validate.sh +94 -0
  201. package/skills/parallel-deep-research/SKILL.md +108 -108
  202. package/skills/parallel-deep-research/evals/README.md +27 -27
  203. package/skills/parallel-deep-research/evals/evals.json +62 -62
  204. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  205. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  206. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  207. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  208. package/.cursor/hooks/state/continual-learning-index.json +0 -19
  209. 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,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"
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"
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 "$@"
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('node:fs');
4
+
5
+ const { analyzePhase3Dependencies } = require('../lib/dep-guard/analyzer.js');
6
+
7
+ function readJsonFile(filePath) {
8
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
9
+ }
10
+
11
+ function normalizeListPayload(payload) {
12
+ if (Array.isArray(payload)) {
13
+ return payload;
14
+ }
15
+
16
+ if (payload && typeof payload === 'object') {
17
+ return [payload];
18
+ }
19
+
20
+ return [];
21
+ }
22
+
23
+ function normalizeSingleIssuePayload(payload) {
24
+ if (Array.isArray(payload)) {
25
+ return payload[0] ?? {};
26
+ }
27
+
28
+ if (payload && typeof payload === 'object') {
29
+ return payload;
30
+ }
31
+
32
+ return {};
33
+ }
34
+
35
+ function main() {
36
+ const [
37
+ currentIssueFile,
38
+ openIssuesFile,
39
+ inProgressIssuesFile,
40
+ taskFile,
41
+ repositoryRoot = process.cwd(),
42
+ ] = process.argv.slice(2);
43
+
44
+ if (!currentIssueFile || !openIssuesFile || !inProgressIssuesFile || !taskFile) {
45
+ throw new Error(
46
+ 'Usage: dep-guard-analyze.js <current-issue.json> <open-issues.json> <in-progress-issues.json> <task-file> [repository-root]',
47
+ );
48
+ }
49
+
50
+ const currentIssue = normalizeSingleIssuePayload(readJsonFile(currentIssueFile));
51
+ const openIssues = [
52
+ ...normalizeListPayload(readJsonFile(openIssuesFile)),
53
+ ...normalizeListPayload(readJsonFile(inProgressIssuesFile)),
54
+ ].filter((issue) => issue && issue.id && issue.id !== currentIssue.id);
55
+
56
+ const result = analyzePhase3Dependencies({
57
+ currentIssue,
58
+ openIssues,
59
+ taskFile,
60
+ repositoryRoot,
61
+ });
62
+
63
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
64
+ }
65
+
66
+ try {
67
+ main();
68
+ } catch (error) {
69
+ process.stderr.write(`${error.message}\n`);
70
+ process.exit(1);
71
+ }