forge-workflow 0.0.5 → 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 +6 -1
- package/.claude/commands/plan.md +59 -14
- package/.claude/commands/premerge.md +10 -0
- package/.claude/commands/review.md +7 -1
- package/.claude/commands/ship.md +95 -47
- package/.claude/commands/status.md +42 -0
- package/.claude/commands/validate.md +7 -1
- package/.claude/commands/verify.md +52 -4
- package/.claude/rules/workflow.md +16 -0
- package/.claude/scripts/greptile-resolve.sh +32 -0
- package/.cline/workflows/dev.md +6 -1
- package/.cline/workflows/plan.md +59 -14
- package/.cline/workflows/premerge.md +10 -0
- package/.cline/workflows/review.md +7 -1
- package/.cline/workflows/ship.md +95 -47
- package/.cline/workflows/status.md +42 -0
- package/.cline/workflows/validate.md +7 -1
- package/.cline/workflows/verify.md +52 -4
- package/.codex/skills/dev/SKILL.md +6 -1
- package/.codex/skills/plan/SKILL.md +59 -14
- package/.codex/skills/premerge/SKILL.md +10 -0
- package/.codex/skills/review/SKILL.md +7 -1
- package/.codex/skills/ship/SKILL.md +95 -47
- package/.codex/skills/status/SKILL.md +42 -0
- package/.codex/skills/validate/SKILL.md +7 -1
- package/.codex/skills/verify/SKILL.md +52 -4
- package/.cursor/commands/dev.md +6 -1
- package/.cursor/commands/plan.md +59 -14
- package/.cursor/commands/premerge.md +10 -0
- package/.cursor/commands/review.md +7 -1
- package/.cursor/commands/ship.md +95 -47
- package/.cursor/commands/status.md +42 -0
- package/.cursor/commands/validate.md +7 -1
- package/.cursor/commands/verify.md +52 -4
- package/.github/prompts/dev.prompt.md +6 -1
- package/.github/prompts/plan.prompt.md +59 -14
- package/.github/prompts/premerge.prompt.md +10 -0
- package/.github/prompts/review.prompt.md +7 -1
- package/.github/prompts/ship.prompt.md +95 -47
- package/.github/prompts/status.prompt.md +42 -0
- package/.github/prompts/validate.prompt.md +7 -1
- package/.github/prompts/verify.prompt.md +52 -4
- package/.kilocode/workflows/dev.md +6 -1
- package/.kilocode/workflows/plan.md +59 -14
- package/.kilocode/workflows/premerge.md +10 -0
- package/.kilocode/workflows/review.md +7 -1
- package/.kilocode/workflows/ship.md +95 -47
- package/.kilocode/workflows/status.md +42 -0
- package/.kilocode/workflows/validate.md +7 -1
- package/.kilocode/workflows/verify.md +52 -4
- package/.opencode/commands/dev.md +6 -1
- package/.opencode/commands/plan.md +59 -14
- package/.opencode/commands/premerge.md +10 -0
- package/.opencode/commands/review.md +7 -1
- package/.opencode/commands/ship.md +95 -47
- package/.opencode/commands/status.md +42 -0
- package/.opencode/commands/validate.md +7 -1
- package/.opencode/commands/verify.md +52 -4
- package/.roo/commands/dev.md +6 -1
- package/.roo/commands/plan.md +59 -14
- package/.roo/commands/premerge.md +10 -0
- package/.roo/commands/review.md +7 -1
- package/.roo/commands/ship.md +95 -47
- package/.roo/commands/status.md +42 -0
- package/.roo/commands/validate.md +7 -1
- package/.roo/commands/verify.md +52 -4
- package/AGENTS.md +97 -0
- package/CLAUDE.md +10 -0
- package/README.md +2 -2
- package/bin/forge-cmd.js +5 -1
- package/bin/forge-preflight.js +15 -2
- package/bin/forge.js +178 -9
- package/docs/ENHANCED_ONBOARDING.md +96 -86
- package/docs/ROADMAP.md +2 -2
- package/docs/TOOLCHAIN.md +23 -0
- package/docs/VALIDATION.md +1 -1
- package/lefthook.yml +11 -0
- package/lib/agents-config.js +2 -2
- package/lib/commands/_registry.js +134 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/dev.js +58 -0
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +1 -1
- package/lib/commands/sync.js +55 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/worktree.js +310 -0
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/freshness-token.js +148 -0
- package/lib/greptile-match.js +80 -0
- package/lib/reset.js +309 -0
- package/lib/task-ownership.js +117 -0
- package/package.json +2 -2
- package/scripts/beads-context.sh +157 -22
- package/scripts/beads-context.test.js +5 -1
- package/scripts/check-forge-token.js +98 -0
- package/scripts/conflict-detect.sh +2 -2
- package/scripts/dep-guard.sh +6 -28
- package/scripts/file-index.sh +117 -23
- 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/lib/eval-runner.js +39 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/pr-coordinator.sh +706 -0
- package/scripts/smart-status.sh +37 -10
- package/scripts/sync-utils.sh +24 -29
- package/scripts/test.js +3 -1
package/scripts/beads-context.sh
CHANGED
|
@@ -7,13 +7,18 @@
|
|
|
7
7
|
# set-acceptance <issue-id> "<criteria-text>"
|
|
8
8
|
# update-progress <issue-id> <task-num> <total> "<title>" <commit-sha> <test-count> <gate-count>
|
|
9
9
|
# parse-progress <issue-id>
|
|
10
|
-
# stage-transition <issue-id> <completed-stage> <next-stage>
|
|
10
|
+
# stage-transition <issue-id> <completed-stage> <next-stage> [--summary "..."] [--decisions "..."] [--artifacts "..."] [--next "..."]
|
|
11
|
+
# validate <issue-id>
|
|
11
12
|
#
|
|
12
13
|
# Cross-platform: works on Windows (Git Bash), macOS, and Linux.
|
|
13
14
|
# OWASP A03: All variables properly quoted to prevent shell injection.
|
|
14
15
|
|
|
15
16
|
set -euo pipefail
|
|
16
17
|
|
|
18
|
+
# Source shared sanitize library
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
+
source "$SCRIPT_DIR/lib/sanitize.sh"
|
|
21
|
+
|
|
17
22
|
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
18
23
|
|
|
19
24
|
usage() {
|
|
@@ -25,7 +30,8 @@ Subcommands:
|
|
|
25
30
|
set-acceptance <issue-id> "<criteria-text>"
|
|
26
31
|
update-progress <issue-id> <task-num> <total> "<title>" <commit-sha> <test-count> <gate-count>
|
|
27
32
|
parse-progress <issue-id>
|
|
28
|
-
stage-transition <issue-id> <completed-stage> <next-stage>
|
|
33
|
+
stage-transition <issue-id> <completed-stage> <next-stage> [--summary "..."] [--decisions "..."] [--artifacts "..."] [--next "..."]
|
|
34
|
+
validate <issue-id>
|
|
29
35
|
EOF
|
|
30
36
|
exit 1
|
|
31
37
|
}
|
|
@@ -35,24 +41,6 @@ die() {
|
|
|
35
41
|
exit 1
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
# Sanitize a string: strip shell-injection patterns (OWASP A03)
|
|
39
|
-
# Removes: double quotes, $(...), backticks, semicolons, and newlines
|
|
40
|
-
sanitize() {
|
|
41
|
-
local val="$1"
|
|
42
|
-
# Remove double quotes
|
|
43
|
-
val="${val//\"/}"
|
|
44
|
-
# Remove $(...) command substitution patterns (loop handles nested)
|
|
45
|
-
# Use newline-separated commands for BSD sed compatibility (macOS)
|
|
46
|
-
val="$(printf '%s' "$val" | sed -e ':loop' -e 's/\$([^()]*)//g' -e 't loop')"
|
|
47
|
-
# Remove backtick command substitution
|
|
48
|
-
val="${val//\`/}"
|
|
49
|
-
# Remove semicolons (command chaining)
|
|
50
|
-
val="${val//;/}"
|
|
51
|
-
# Replace newlines with spaces
|
|
52
|
-
val="$(printf '%s' "$val" | tr '\n' ' ')"
|
|
53
|
-
printf '%s' "$val"
|
|
54
|
-
}
|
|
55
|
-
|
|
56
44
|
# Run bd update and check for errors in both exit code and output.
|
|
57
45
|
# bd update exits 0 even for non-existent issues, so we check stdout too.
|
|
58
46
|
bd_update() {
|
|
@@ -250,7 +238,7 @@ cmd_parse_progress() {
|
|
|
250
238
|
|
|
251
239
|
cmd_stage_transition() {
|
|
252
240
|
if [[ $# -lt 3 ]]; then
|
|
253
|
-
echo "Usage: beads-context.sh stage-transition <issue-id> <completed-stage> <next-stage>" >&2
|
|
241
|
+
echo "Usage: beads-context.sh stage-transition <issue-id> <completed-stage> <next-stage> [--summary \"...\"] [--decisions \"...\"] [--artifacts \"...\"] [--next \"...\"]" >&2
|
|
254
242
|
exit 1
|
|
255
243
|
fi
|
|
256
244
|
|
|
@@ -259,9 +247,56 @@ cmd_stage_transition() {
|
|
|
259
247
|
completed="$(sanitize "$2")"
|
|
260
248
|
local next
|
|
261
249
|
next="$(sanitize "$3")"
|
|
262
|
-
|
|
250
|
+
shift 3
|
|
251
|
+
|
|
252
|
+
# Parse optional flags
|
|
253
|
+
local flag_summary="" flag_decisions="" flag_artifacts="" flag_next=""
|
|
254
|
+
while [[ $# -gt 0 ]]; do
|
|
255
|
+
case "$1" in
|
|
256
|
+
--summary)
|
|
257
|
+
shift
|
|
258
|
+
flag_summary="$(sanitize "${1:-}")"
|
|
259
|
+
;;
|
|
260
|
+
--decisions)
|
|
261
|
+
shift
|
|
262
|
+
flag_decisions="$(sanitize "${1:-}")"
|
|
263
|
+
;;
|
|
264
|
+
--artifacts)
|
|
265
|
+
shift
|
|
266
|
+
flag_artifacts="$(sanitize "${1:-}")"
|
|
267
|
+
;;
|
|
268
|
+
--next)
|
|
269
|
+
shift
|
|
270
|
+
flag_next="$(sanitize "${1:-}")"
|
|
271
|
+
;;
|
|
272
|
+
*)
|
|
273
|
+
# Ignore unknown flags for forward compatibility
|
|
274
|
+
;;
|
|
275
|
+
esac
|
|
276
|
+
shift
|
|
277
|
+
done
|
|
278
|
+
|
|
279
|
+
# Build comment: header line always present
|
|
263
280
|
local comment="Stage: ${completed} complete → ready for ${next}"
|
|
264
281
|
|
|
282
|
+
# Append structured fields only if provided
|
|
283
|
+
if [[ -n "$flag_summary" ]]; then
|
|
284
|
+
comment="${comment}
|
|
285
|
+
Summary: ${flag_summary}"
|
|
286
|
+
fi
|
|
287
|
+
if [[ -n "$flag_decisions" ]]; then
|
|
288
|
+
comment="${comment}
|
|
289
|
+
Decisions: ${flag_decisions}"
|
|
290
|
+
fi
|
|
291
|
+
if [[ -n "$flag_artifacts" ]]; then
|
|
292
|
+
comment="${comment}
|
|
293
|
+
Artifacts: ${flag_artifacts}"
|
|
294
|
+
fi
|
|
295
|
+
if [[ -n "$flag_next" ]]; then
|
|
296
|
+
comment="${comment}
|
|
297
|
+
Next: ${flag_next}"
|
|
298
|
+
fi
|
|
299
|
+
|
|
265
300
|
if ! bd_comment "$issue_id" "$comment" > /dev/null; then
|
|
266
301
|
die "Failed to record stage transition on issue ${issue_id}"
|
|
267
302
|
fi
|
|
@@ -269,6 +304,105 @@ cmd_stage_transition() {
|
|
|
269
304
|
echo "Stage transition recorded on ${issue_id}: ${completed} → ${next}"
|
|
270
305
|
}
|
|
271
306
|
|
|
307
|
+
cmd_validate() {
|
|
308
|
+
if [[ $# -lt 1 ]]; then
|
|
309
|
+
echo "Usage: beads-context.sh validate <issue-id>" >&2
|
|
310
|
+
exit 1
|
|
311
|
+
fi
|
|
312
|
+
|
|
313
|
+
local issue_id="$1"
|
|
314
|
+
local warnings=0
|
|
315
|
+
|
|
316
|
+
# Get issue JSON
|
|
317
|
+
local json
|
|
318
|
+
json="$(bd show "$issue_id" --json 2>&1)" || {
|
|
319
|
+
echo "Error: Failed to retrieve issue ${issue_id}" >&2
|
|
320
|
+
exit 1
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# Check for bd error patterns
|
|
324
|
+
if printf '%s' "$json" | grep -Eqi '^Error|Error resolving|Error fetching'; then
|
|
325
|
+
echo "Error: Issue not found: ${issue_id}" >&2
|
|
326
|
+
exit 1
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
if [[ -z "$json" || "$json" == "[]" || "$json" == "null" ]]; then
|
|
330
|
+
echo "Error: Issue not found: ${issue_id}" >&2
|
|
331
|
+
exit 1
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
# Extract fields — prefer jq, fall back to grep/sed
|
|
335
|
+
local description="" design=""
|
|
336
|
+
if command -v jq &>/dev/null; then
|
|
337
|
+
description="$(printf '%s' "$json" | jq -r 'if type == "array" then .[0].description else .description end // empty' 2>/dev/null || true)"
|
|
338
|
+
design="$(printf '%s' "$json" | jq -r 'if type == "array" then .[0].design else .design end // empty' 2>/dev/null || true)"
|
|
339
|
+
else
|
|
340
|
+
description="$(printf '%s' "$json" | grep -o '"description": *"[^"]*"' | head -1 | sed 's/^"description": *"//;s/"$//' || true)"
|
|
341
|
+
design="$(printf '%s' "$json" | grep -o '"design": *"[^"]*"' | head -1 | sed 's/^"design": *"//;s/"$//' || true)"
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
# Check 1: Issue has description
|
|
345
|
+
if [[ -z "$description" ]]; then
|
|
346
|
+
echo "Warning: Issue ${issue_id} has no description"
|
|
347
|
+
warnings=$((warnings + 1))
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
# Get comments to check for stage transitions
|
|
351
|
+
local comments
|
|
352
|
+
comments="$(bd comments list "$issue_id" 2>/dev/null || true)"
|
|
353
|
+
|
|
354
|
+
# Check 2: At least one stage transition exists
|
|
355
|
+
local has_transition=false
|
|
356
|
+
if echo "$comments" | grep -q 'Stage:.*complete'; then
|
|
357
|
+
has_transition=true
|
|
358
|
+
fi
|
|
359
|
+
|
|
360
|
+
if [[ "$has_transition" == "false" ]]; then
|
|
361
|
+
echo "Warning: No stage transition found on issue ${issue_id}"
|
|
362
|
+
warnings=$((warnings + 1))
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
# Check 3: Most recent transition has summary
|
|
366
|
+
# Isolate the last transition block to avoid false-positives from earlier summaries
|
|
367
|
+
if [[ "$has_transition" == "true" ]]; then
|
|
368
|
+
local has_summary=false
|
|
369
|
+
local last_block
|
|
370
|
+
last_block="$(echo "$comments" | awk '/Stage:.*complete/{found=1; block=""} found{block=block"\n"$0} END{print block}')"
|
|
371
|
+
if echo "$last_block" | grep -q 'Summary:'; then
|
|
372
|
+
has_summary=true
|
|
373
|
+
fi
|
|
374
|
+
if [[ "$has_summary" == "false" ]]; then
|
|
375
|
+
echo "Warning: Most recent stage transition has no summary on issue ${issue_id}"
|
|
376
|
+
warnings=$((warnings + 1))
|
|
377
|
+
fi
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
# Check 4: Design metadata is set if past plan stage
|
|
381
|
+
# "Past plan" means there's a transition FROM plan or any later stage
|
|
382
|
+
local past_plan=false
|
|
383
|
+
if echo "$comments" | grep -q 'Stage: plan complete'; then
|
|
384
|
+
past_plan=true
|
|
385
|
+
fi
|
|
386
|
+
if echo "$comments" | grep -Eq 'Stage: (dev|validate|ship|review|premerge) complete'; then
|
|
387
|
+
past_plan=true
|
|
388
|
+
fi
|
|
389
|
+
|
|
390
|
+
if [[ "$past_plan" == "true" && -z "$design" ]]; then
|
|
391
|
+
echo "Warning: Design metadata not set on issue ${issue_id} (past plan stage)"
|
|
392
|
+
warnings=$((warnings + 1))
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
# Summary
|
|
396
|
+
if [[ $warnings -eq 0 ]]; then
|
|
397
|
+
echo "All context fields present on issue ${issue_id}"
|
|
398
|
+
else
|
|
399
|
+
echo "${warnings} warning(s) found on issue ${issue_id}"
|
|
400
|
+
fi
|
|
401
|
+
|
|
402
|
+
# Always exit 0 — advisory only
|
|
403
|
+
exit 0
|
|
404
|
+
}
|
|
405
|
+
|
|
272
406
|
# ── Main dispatcher ──────────────────────────────────────────────────────
|
|
273
407
|
|
|
274
408
|
if [[ $# -lt 1 ]]; then
|
|
@@ -284,6 +418,7 @@ case "$subcommand" in
|
|
|
284
418
|
update-progress) cmd_update_progress "$@" ;;
|
|
285
419
|
parse-progress) cmd_parse_progress "$@" ;;
|
|
286
420
|
stage-transition) cmd_stage_transition "$@" ;;
|
|
421
|
+
validate) cmd_validate "$@" ;;
|
|
287
422
|
*)
|
|
288
423
|
echo "Error: Unknown subcommand '${subcommand}'" >&2
|
|
289
424
|
usage
|
|
@@ -8,10 +8,14 @@ const { resolveBashCommand } = require('../test/helpers/bash.js');
|
|
|
8
8
|
const SCRIPT_PATH = path.join(__dirname, 'beads-context.sh');
|
|
9
9
|
const WORKTREE_ROOT = path.resolve(__dirname, '..');
|
|
10
10
|
|
|
11
|
-
// Check if bd CLI is available (not
|
|
11
|
+
// Check if bd CLI is available AND Dolt database is reachable (not just binary exists).
|
|
12
|
+
// Prevents tests from hanging when Dolt is dead. BD_TIMEOUT env var overrides default.
|
|
12
13
|
function isBdAvailable() {
|
|
13
14
|
try {
|
|
14
15
|
execFileSync('bd', ['--version'], { stdio: 'ignore' });
|
|
16
|
+
// Also verify Dolt database is reachable (not just binary exists)
|
|
17
|
+
const timeout = parseInt(process.env.BD_TIMEOUT || '3000', 10);
|
|
18
|
+
execFileSync('bd', ['list', '--limit=1'], { stdio: 'ignore', timeout });
|
|
15
19
|
return true;
|
|
16
20
|
} catch {
|
|
17
21
|
return false;
|
|
@@ -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
|
+
}
|
|
@@ -19,12 +19,12 @@ set -euo pipefail
|
|
|
19
19
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
20
|
|
|
21
21
|
# Source file-index helpers (file_index_read, file_index_get)
|
|
22
|
-
source "$SCRIPT_DIR/file-index.sh"
|
|
22
|
+
source "$SCRIPT_DIR/file-index.sh" || { echo "FATAL: failed to source file-index.sh" >&2; exit 2; }
|
|
23
23
|
|
|
24
24
|
# Source sync-utils (get_session_identity, sanitize_config_value)
|
|
25
25
|
# Only source if available (tests may not need all utils)
|
|
26
26
|
if [[ -f "$SCRIPT_DIR/sync-utils.sh" ]]; then
|
|
27
|
-
source "$SCRIPT_DIR/sync-utils.sh"
|
|
27
|
+
source "$SCRIPT_DIR/sync-utils.sh" || { echo "FATAL: failed to source sync-utils.sh" >&2; exit 2; }
|
|
28
28
|
fi
|
|
29
29
|
|
|
30
30
|
# ── Input Validation ─────────────────────────────────────────────────────
|
package/scripts/dep-guard.sh
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
set -euo pipefail
|
|
14
14
|
|
|
15
|
+
# Source shared sanitize library
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
source "$SCRIPT_DIR/lib/sanitize.sh"
|
|
18
|
+
|
|
15
19
|
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
16
20
|
|
|
17
21
|
usage() {
|
|
@@ -64,24 +68,6 @@ rollback_and_die() {
|
|
|
64
68
|
die "$message"
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
# Sanitize a string: strip shell-injection patterns (OWASP A03)
|
|
68
|
-
# Removes: double quotes, $(...), backticks, semicolons, and newlines
|
|
69
|
-
sanitize() {
|
|
70
|
-
local val="$1"
|
|
71
|
-
# Remove double quotes
|
|
72
|
-
val="${val//\"/}"
|
|
73
|
-
# Remove $(...) command substitution patterns (loop handles nested)
|
|
74
|
-
# Use newline-separated commands for BSD sed compatibility (macOS)
|
|
75
|
-
val="$(printf '%s' "$val" | sed -e ':loop' -e 's/\$([^()]*)//g' -e 't loop')"
|
|
76
|
-
# Remove backtick command substitution
|
|
77
|
-
val="${val//\`/}"
|
|
78
|
-
# Remove semicolons (command chaining)
|
|
79
|
-
val="${val//;/}"
|
|
80
|
-
# Replace newlines with spaces
|
|
81
|
-
val="$(printf '%s' "$val" | tr '\n' ' ')"
|
|
82
|
-
printf '%s' "$val"
|
|
83
|
-
}
|
|
84
|
-
|
|
85
71
|
# Run bd update and check for errors in both exit code and output.
|
|
86
72
|
# bd update exits 0 even for non-existent issues, so we check stdout too.
|
|
87
73
|
bd_update() {
|
|
@@ -628,17 +614,9 @@ cmd_apply_decision() {
|
|
|
628
614
|
die "Failed to add dependency ${dependent_issue} -> ${depends_on_issue}"
|
|
629
615
|
}
|
|
630
616
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
rollback_dependency "$dependent_issue" "$depends_on_issue"
|
|
634
|
-
echo "$cycles_output" >&2
|
|
635
|
-
die "Failed to validate dependency cycles"
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
if printf '%s' "$cycles_output" | grep -Eqi 'cycle' \
|
|
639
|
-
&& ! cycles_output_is_safe "$cycles_output"; then
|
|
617
|
+
# Check for cycles — use exit code as primary signal
|
|
618
|
+
if ! ${BD_CMD:-bd} dep cycles &>/dev/null; then
|
|
640
619
|
rollback_dependency "$dependent_issue" "$depends_on_issue"
|
|
641
|
-
echo "$cycles_output" >&2
|
|
642
620
|
die "Cycle detected for ${dependent_issue} -> ${depends_on_issue}"
|
|
643
621
|
fi
|
|
644
622
|
|
package/scripts/file-index.sh
CHANGED
|
@@ -19,6 +19,13 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
|
19
19
|
set -euo pipefail
|
|
20
20
|
fi
|
|
21
21
|
|
|
22
|
+
# Source shared libraries
|
|
23
|
+
_FI_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
source "$_FI_SCRIPT_DIR/lib/sanitize.sh"
|
|
25
|
+
if [[ -f "$_FI_SCRIPT_DIR/lib/jsonl-lock.sh" ]]; then
|
|
26
|
+
source "$_FI_SCRIPT_DIR/lib/jsonl-lock.sh"
|
|
27
|
+
fi
|
|
28
|
+
|
|
22
29
|
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
23
30
|
|
|
24
31
|
# Resolve the JSONL file path.
|
|
@@ -28,24 +35,6 @@ _file_index_path() {
|
|
|
28
35
|
printf '%s' "$root/.beads/file-index.jsonl"
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
# Sanitize a string: strip shell-injection patterns (OWASP A03)
|
|
32
|
-
# Reused from dep-guard.sh pattern.
|
|
33
|
-
# Removes: double quotes, $(...), backticks, semicolons, and newlines
|
|
34
|
-
sanitize() {
|
|
35
|
-
local val="$1"
|
|
36
|
-
# Remove double quotes
|
|
37
|
-
val="${val//\"/}"
|
|
38
|
-
# Remove $(...) command substitution patterns (loop handles nested)
|
|
39
|
-
val="$(printf '%s' "$val" | sed -e ':loop' -e 's/\$([^()]*)//g' -e 't loop')"
|
|
40
|
-
# Remove backtick command substitution
|
|
41
|
-
val="${val//\`/}"
|
|
42
|
-
# Remove semicolons (command chaining)
|
|
43
|
-
val="${val//;/}"
|
|
44
|
-
# Replace newlines with spaces
|
|
45
|
-
val="$(printf '%s' "$val" | tr '\n' ' ')"
|
|
46
|
-
printf '%s' "$val"
|
|
47
|
-
}
|
|
48
|
-
|
|
49
38
|
# Validate issue_id: alphanumeric + hyphens only
|
|
50
39
|
_validate_issue_id() {
|
|
51
40
|
local id="$1"
|
|
@@ -127,7 +116,11 @@ file_index_add() {
|
|
|
127
116
|
# Ensure parent directory exists
|
|
128
117
|
mkdir -p "$(dirname "$jsonl_path")"
|
|
129
118
|
|
|
130
|
-
|
|
119
|
+
if command -v atomic_jsonl_append &>/dev/null; then
|
|
120
|
+
atomic_jsonl_append "$jsonl_path" "$json_line"
|
|
121
|
+
else
|
|
122
|
+
printf '%s\n' "$json_line" >> "$jsonl_path"
|
|
123
|
+
fi
|
|
131
124
|
}
|
|
132
125
|
|
|
133
126
|
# file_index_remove <issue_id>
|
|
@@ -163,7 +156,11 @@ file_index_remove() {
|
|
|
163
156
|
local jsonl_path
|
|
164
157
|
jsonl_path="$(_file_index_path)"
|
|
165
158
|
mkdir -p "$(dirname "$jsonl_path")"
|
|
166
|
-
|
|
159
|
+
if command -v atomic_jsonl_append &>/dev/null; then
|
|
160
|
+
atomic_jsonl_append "$jsonl_path" "$json_line"
|
|
161
|
+
else
|
|
162
|
+
printf '%s\n' "$json_line" >> "$jsonl_path"
|
|
163
|
+
fi
|
|
167
164
|
}
|
|
168
165
|
|
|
169
166
|
# file_index_read
|
|
@@ -320,12 +317,28 @@ file_index_update_from_tasks() {
|
|
|
320
317
|
# Build deduplicated JSON arrays using jq (no associative arrays needed)
|
|
321
318
|
local files_json modules_json
|
|
322
319
|
if [[ -n "$sanitized_paths" ]]; then
|
|
323
|
-
|
|
320
|
+
# Save and restore pipefail state to avoid side effects
|
|
321
|
+
local _prev_pipefail
|
|
322
|
+
_prev_pipefail="$(set +o | grep pipefail)" || true
|
|
323
|
+
set -o pipefail
|
|
324
|
+
|
|
325
|
+
files_json="$(printf '%s' "$sanitized_paths" | grep -v '^$' | jq -R . | jq -s -c 'unique')" || {
|
|
326
|
+
eval "$_prev_pipefail"
|
|
327
|
+
echo "Error: failed to build files JSON array" >&2
|
|
328
|
+
return 1
|
|
329
|
+
}
|
|
324
330
|
modules_json="$(printf '%s' "$sanitized_paths" | grep -v '^$' | while IFS= read -r p; do
|
|
325
331
|
local d
|
|
326
332
|
d="$(dirname "$p")"
|
|
327
333
|
if [[ "$d" == "." ]]; then printf '%s\n' "./"; else printf '%s\n' "${d}/"; fi
|
|
328
|
-
done | jq -R . | jq -s -c 'unique')"
|
|
334
|
+
done | jq -R . | jq -s -c 'unique')" || {
|
|
335
|
+
eval "$_prev_pipefail"
|
|
336
|
+
echo "Error: failed to build modules JSON array" >&2
|
|
337
|
+
return 1
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# Restore pipefail state
|
|
341
|
+
eval "$_prev_pipefail"
|
|
329
342
|
else
|
|
330
343
|
files_json="[]"
|
|
331
344
|
modules_json="[]"
|
|
@@ -358,13 +371,92 @@ file_index_update_from_tasks() {
|
|
|
358
371
|
tombstone: false,
|
|
359
372
|
confidence: $conf
|
|
360
373
|
}')"
|
|
361
|
-
|
|
374
|
+
if command -v atomic_jsonl_append &>/dev/null; then
|
|
375
|
+
atomic_jsonl_append "$jsonl_path" "$json_line"
|
|
376
|
+
else
|
|
377
|
+
printf '%s\n' "$json_line" >> "$jsonl_path"
|
|
378
|
+
fi
|
|
362
379
|
else
|
|
363
380
|
# Normal entry via file_index_add (no confidence field needed)
|
|
364
381
|
file_index_add "$issue_id" "$developer" "$files_json" "$modules_json"
|
|
365
382
|
fi
|
|
366
383
|
}
|
|
367
384
|
|
|
385
|
+
# file_index_auto_update <issue_id> [--from-git]
|
|
386
|
+
# Update the file index with the current set of changed files.
|
|
387
|
+
# Files come from: --from-git flag (git diff HEAD~1) or stdin.
|
|
388
|
+
file_index_auto_update() {
|
|
389
|
+
local raw_issue_id="$1"
|
|
390
|
+
shift
|
|
391
|
+
local from_git=0
|
|
392
|
+
|
|
393
|
+
# Parse flags
|
|
394
|
+
while [[ $# -gt 0 ]]; do
|
|
395
|
+
case "$1" in
|
|
396
|
+
--from-git) from_git=1; shift ;;
|
|
397
|
+
*) echo "Error: unknown flag '$1'" >&2; return 1 ;;
|
|
398
|
+
esac
|
|
399
|
+
done
|
|
400
|
+
|
|
401
|
+
# Sanitize and validate issue_id
|
|
402
|
+
local issue_id
|
|
403
|
+
issue_id="$(sanitize "$raw_issue_id")"
|
|
404
|
+
issue_id="$(printf '%s' "$issue_id" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
405
|
+
_validate_issue_id "$issue_id" || return 1
|
|
406
|
+
|
|
407
|
+
# Get file list
|
|
408
|
+
local -a files=()
|
|
409
|
+
if [[ "$from_git" -eq 1 ]]; then
|
|
410
|
+
while IFS= read -r f; do
|
|
411
|
+
[[ -n "$f" ]] && files+=("$f")
|
|
412
|
+
done < <(git diff --name-only HEAD~1 2>/dev/null || true)
|
|
413
|
+
else
|
|
414
|
+
# Read from stdin (non-blocking check)
|
|
415
|
+
if [[ ! -t 0 ]]; then
|
|
416
|
+
while IFS= read -r f; do
|
|
417
|
+
[[ -n "$f" ]] && files+=("$f")
|
|
418
|
+
done
|
|
419
|
+
else
|
|
420
|
+
echo "Error: no file list provided (use --from-git or pipe files via stdin)" >&2
|
|
421
|
+
return 1
|
|
422
|
+
fi
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
# Empty file list = nothing to do
|
|
426
|
+
if [[ ${#files[@]} -eq 0 ]]; then
|
|
427
|
+
echo "No files changed"
|
|
428
|
+
return 0
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
# Get session identity
|
|
432
|
+
local developer
|
|
433
|
+
if ! command -v get_session_identity &>/dev/null; then
|
|
434
|
+
local script_dir
|
|
435
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
436
|
+
if [[ -f "$script_dir/sync-utils.sh" ]]; then
|
|
437
|
+
source "$script_dir/sync-utils.sh"
|
|
438
|
+
fi
|
|
439
|
+
fi
|
|
440
|
+
developer="$(get_session_identity)" || {
|
|
441
|
+
echo "Error: could not determine session identity" >&2
|
|
442
|
+
return 1
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# Build JSON arrays using jq
|
|
446
|
+
local files_json modules_json
|
|
447
|
+
files_json="$(printf '%s\n' "${files[@]}" | jq -R . | jq -s -c 'unique')"
|
|
448
|
+
modules_json="$(printf '%s\n' "${files[@]}" | while IFS= read -r p; do
|
|
449
|
+
local d
|
|
450
|
+
d="$(dirname "$p")"
|
|
451
|
+
if [[ "$d" == "." ]]; then printf '%s\n' "./"; else printf '%s\n' "${d}/"; fi
|
|
452
|
+
done | jq -R . | jq -s -c 'unique')"
|
|
453
|
+
|
|
454
|
+
# Append via file_index_add (which uses atomic locking)
|
|
455
|
+
file_index_add "$issue_id" "$developer" "$files_json" "$modules_json"
|
|
456
|
+
|
|
457
|
+
echo "File index updated for $issue_id (${#files[@]} files)"
|
|
458
|
+
}
|
|
459
|
+
|
|
368
460
|
# ── Main dispatcher (when run as a script) ────────────────────────────────
|
|
369
461
|
|
|
370
462
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
@@ -378,6 +470,7 @@ Subcommands:
|
|
|
378
470
|
read
|
|
379
471
|
get <issue_id>
|
|
380
472
|
update-from-tasks <issue_id> <task_file_path> [action]
|
|
473
|
+
auto-update <issue_id> [--from-git]
|
|
381
474
|
EOF
|
|
382
475
|
exit 1
|
|
383
476
|
fi
|
|
@@ -391,6 +484,7 @@ EOF
|
|
|
391
484
|
read) file_index_read ;;
|
|
392
485
|
get) file_index_get "$@" ;;
|
|
393
486
|
update-from-tasks) file_index_update_from_tasks "$@" ;;
|
|
487
|
+
auto-update) file_index_auto_update "$@" ;;
|
|
394
488
|
*)
|
|
395
489
|
echo "Error: Unknown subcommand '${subcommand}'" >&2
|
|
396
490
|
exit 1
|