@moretea-labs/forge 1.7.0 → 1.7.1
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/AGENTS.md +2 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +2 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/docs/architecture/CURRENT.md +12 -4
- package/docs/forge-plugin-management.md +1 -1
- package/docs/operations/controller-performance-and-502.md +19 -0
- package/docs/operations/homebrew.md +2 -2
- package/package.json +5 -2
- package/scripts/architecture-event.ts +2 -820
- package/scripts/architecture-queue.sh +2 -645
- package/scripts/archive-architecture-request.sh +2 -242
- package/scripts/archive-workflow.sh +1 -291
- package/scripts/benchmark-work-efficiency.ts +0 -0
- package/scripts/capability-config.ts +2 -392
- package/scripts/capability-resolver.ts +2 -503
- package/scripts/capture-plan.sh +1 -332
- package/scripts/check-agent-tooling.sh +2 -1531
- package/scripts/check-architecture-sync.sh +2 -302
- package/scripts/check-brain-manifest.sh +2 -253
- package/scripts/check-context-files.sh +2 -47
- package/scripts/check-deploy-sql-order.sh +2 -115
- package/scripts/check-runtime-architecture.mjs +243 -16
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +0 -0
- package/scripts/check-task-sync.sh +2 -63
- package/scripts/check-task-workflow.sh +2 -1000
- package/scripts/codex-handoff-resume.sh +2 -267
- package/scripts/context-contract-sync.sh +2 -514
- package/scripts/contract-run.ts +2 -463
- package/scripts/contract-worktree.sh +1 -702
- package/scripts/ensure-task-workflow.sh +1 -1192
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/lib/project-init-lib.sh +7 -2
- package/scripts/lib/run-forge-helper.sh +19 -0
- package/scripts/lib/run-forge-helper.ts +26 -0
- package/scripts/migrate-project-template.sh +1 -33
- package/scripts/migrate-workflow-docs.ts +4 -412
- package/scripts/new-plan.sh +1 -180
- package/scripts/new-spec.sh +1 -47
- package/scripts/new-sprint.sh +1 -46
- package/scripts/plan-to-todo.sh +1 -771
- package/scripts/prepare-codex-handoff.sh +2 -201
- package/scripts/prepare-handoff.sh +1 -78
- package/scripts/refresh-current-status.sh +2 -467
- package/scripts/run-governed-gate.ts +3 -0
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -531
- package/scripts/sprint-backlog.sh +1 -834
- package/scripts/stage-runtime-release.ts +2 -1
- package/scripts/summarize-failures.sh +2 -123
- package/scripts/switch-plan.sh +1 -150
- package/scripts/sync-brain-docs.sh +2 -379
- package/scripts/sync-generated-authority.mjs +175 -0
- package/scripts/verify-contract.sh +2 -593
- package/scripts/verify-sprint.sh +1 -500
- package/scripts/workflow-contract.ts +4 -201
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/controller/check-runner.ts +7 -2
- package/src/cli/controller/context-pack.ts +46 -21
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +2 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/local-bridge/facade-api.ts +37 -47
- package/src/cli/local-bridge/server.ts +2 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -33
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +85 -89
- package/src/cli/mcp/server.ts +57 -13
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset.ts +2 -2
- package/src/cli/mcp/transports/http.ts +23 -0
- package/src/cli/repositories/command-classifier.ts +2 -2
- package/src/cli/repositories/command-executor.ts +24 -6
- package/src/cli/repositories/repository-snapshot.ts +5 -2
- package/src/cli/repositories/structured-git.ts +57 -3
- package/src/cli/repository/inspector.ts +11 -3
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +9 -61
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
- package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
- package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
- package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
- package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
- package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
- package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
- package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
- package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
- package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
- package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
- package/src/runtime/control-plane/facade/index.ts +2 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
- package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
- package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
- package/src/runtime/control-plane/facade/types.ts +68 -5
- package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
- package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +18 -6
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/process-runtime/check-result.ts +53 -18
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
- package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
- package/src/runtime/execution/process-runtime/runtime.ts +10 -0
- package/src/runtime/execution/process-runtime/types.ts +2 -12
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -13
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/router.ts +2 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
- package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
- package/src/runtime/plugins/browser-adapter.ts +256 -1083
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +18 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
- package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
- package/src/runtime/plugins/browser-native-session-service.ts +222 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +10 -0
- package/src/runtime/plugins/browser-session-store.ts +121 -0
- package/src/runtime/plugins/browser-session-types.ts +98 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
- package/src/runtime/plugins/external-adapter.ts +92 -58
- package/src/runtime/plugins/macos-capability-broker.ts +0 -8
- package/src/runtime/plugins/store.ts +66 -9
- package/src/runtime/plugins/types.ts +3 -1
- package/src/runtime/recovery/maintenance-executor.ts +61 -25
- package/src/runtime/root/package-connector-service.ts +46 -4
- package/src/runtime/root/package-runtime-release.ts +1 -1
- package/src/runtime/root/package-runtime-service.ts +33 -2
- package/src/runtime/root/release-manifest.ts +137 -16
- package/src/runtime/root/release-materialize.ts +109 -35
- package/src/runtime/root/types.ts +10 -0
- package/src/runtime/standalone-recovery/core.ts +425 -194
- package/src/runtime/standalone-recovery/entry.ts +1 -1
- package/src/runtime/standalone-recovery/installer.ts +1 -1
- package/src/runtime/workflow/schedules/engine.ts +113 -87
- package/src/runtime/workflow/schedules/settlement.ts +26 -23
- package/src/runtime/workflow/schedules/store.ts +79 -10
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { realpathSync } from 'fs';
|
|
2
3
|
import { resolve } from 'path';
|
|
3
4
|
import { assertRuntimeReleaseFiles, stageRuntimeRelease, type CandidateRuntimeStageReceiptV1 } from '../src/runtime/root/release-materialize';
|
|
4
5
|
|
|
@@ -18,7 +19,7 @@ const controllerHome = resolve(requiredOption(args, '--controller-home'));
|
|
|
18
19
|
const sourceRoot = resolve(requiredOption(args, '--source-root'));
|
|
19
20
|
const expectedHead = requiredOption(args, '--expected-head');
|
|
20
21
|
if (!/^[a-f0-9]{40}$/i.test(expectedHead)) throw new Error('RUNTIME_RELEASE_CANDIDATE_EXPECTED_HEAD_INVALID');
|
|
21
|
-
if (sourceRoot !==
|
|
22
|
+
if (realpathSync(sourceRoot) !== realpathSync(process.cwd())) throw new Error('RUNTIME_RELEASE_CANDIDATE_CWD_MISMATCH');
|
|
22
23
|
|
|
23
24
|
const staged = stageRuntimeRelease({ controllerHome, sourceRoot });
|
|
24
25
|
assertRuntimeReleaseFiles(staged);
|
|
@@ -1,125 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# shellcheck source=/dev/null
|
|
6
|
-
. ".ai/hooks/lib/workflow-state.sh"
|
|
7
|
-
fi
|
|
8
|
-
|
|
9
|
-
usage() {
|
|
10
|
-
cat <<'USAGE_EOF'
|
|
11
|
-
Usage: scripts/summarize-failures.sh [--file <path>] [--run-id <id>]
|
|
12
|
-
USAGE_EOF
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
resolve_js_runtime() {
|
|
16
|
-
if command -v bun >/dev/null 2>&1; then
|
|
17
|
-
printf 'bun'
|
|
18
|
-
return 0
|
|
19
|
-
fi
|
|
20
|
-
|
|
21
|
-
if [[ -x "${HOME}/.bun/bin/bun" ]]; then
|
|
22
|
-
printf '%s' "${HOME}/.bun/bin/bun"
|
|
23
|
-
return 0
|
|
24
|
-
fi
|
|
25
|
-
|
|
26
|
-
if command -v node >/dev/null 2>&1; then
|
|
27
|
-
printf 'node'
|
|
28
|
-
return 0
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
return 1
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if declare -F workflow_failure_log_file >/dev/null 2>&1; then
|
|
35
|
-
log_file="$(workflow_failure_log_file)"
|
|
36
|
-
else
|
|
37
|
-
log_file=".ai/harness/failures/latest.jsonl"
|
|
38
|
-
fi
|
|
39
|
-
filter_run_id=""
|
|
40
|
-
js_runtime=""
|
|
41
|
-
js_code=""
|
|
42
|
-
|
|
43
|
-
while [[ $# -gt 0 ]]; do
|
|
44
|
-
case "$1" in
|
|
45
|
-
--file)
|
|
46
|
-
[[ -n "${2:-}" ]] || { echo "Error: --file requires a value" >&2; usage; exit 2; }
|
|
47
|
-
log_file="$2"
|
|
48
|
-
shift 2
|
|
49
|
-
;;
|
|
50
|
-
--run-id)
|
|
51
|
-
[[ -n "${2:-}" ]] || { echo "Error: --run-id requires a value" >&2; usage; exit 2; }
|
|
52
|
-
filter_run_id="$2"
|
|
53
|
-
shift 2
|
|
54
|
-
;;
|
|
55
|
-
--help|-h)
|
|
56
|
-
usage
|
|
57
|
-
exit 0
|
|
58
|
-
;;
|
|
59
|
-
*)
|
|
60
|
-
echo "Unknown argument: $1" >&2
|
|
61
|
-
usage
|
|
62
|
-
exit 2
|
|
63
|
-
;;
|
|
64
|
-
esac
|
|
65
|
-
done
|
|
66
|
-
|
|
67
|
-
if [[ ! -f "$log_file" ]]; then
|
|
68
|
-
echo "[FailureSummary] No failure log found at $log_file"
|
|
69
|
-
exit 0
|
|
70
|
-
fi
|
|
71
|
-
|
|
72
|
-
js_runtime="$(resolve_js_runtime || true)"
|
|
73
|
-
if [[ -z "$js_runtime" ]]; then
|
|
74
|
-
echo "[FailureSummary] Missing JavaScript runtime (expected bun or node)" >&2
|
|
75
|
-
exit 1
|
|
76
|
-
fi
|
|
77
|
-
|
|
78
|
-
js_code="$(cat <<'NODE_EOF'
|
|
79
|
-
const fs = require("fs");
|
|
80
|
-
|
|
81
|
-
const file = process.env.JSONL_FILE;
|
|
82
|
-
const filterRunId = process.env.FILTER_RUN_ID || "";
|
|
83
|
-
|
|
84
|
-
const rows = fs
|
|
85
|
-
.readFileSync(file, "utf-8")
|
|
86
|
-
.split("\n")
|
|
87
|
-
.map((line) => line.trim())
|
|
88
|
-
.filter(Boolean)
|
|
89
|
-
.map((line) => JSON.parse(line))
|
|
90
|
-
.filter((row) => !filterRunId || row.run_id === filterRunId);
|
|
91
|
-
|
|
92
|
-
if (rows.length === 0) {
|
|
93
|
-
console.log(
|
|
94
|
-
filterRunId
|
|
95
|
-
? `[FailureSummary] No failure records found for run_id=${filterRunId}`
|
|
96
|
-
: "[FailureSummary] No failure records found"
|
|
97
|
-
);
|
|
98
|
-
process.exit(0);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const classCounts = new Map();
|
|
102
|
-
const guardCounts = new Map();
|
|
103
|
-
|
|
104
|
-
for (const row of rows) {
|
|
105
|
-
classCounts.set(row.failure_class, (classCounts.get(row.failure_class) || 0) + 1);
|
|
106
|
-
guardCounts.set(row.guard, (guardCounts.get(row.guard) || 0) + 1);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const sortEntries = (map) =>
|
|
110
|
-
[...map.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
111
|
-
|
|
112
|
-
const latestRunId = rows[rows.length - 1]?.run_id || "unknown";
|
|
113
|
-
console.log(`[FailureSummary] records=${rows.length} run_id=${filterRunId || latestRunId}`);
|
|
114
|
-
console.log("Failure classes:");
|
|
115
|
-
for (const [name, count] of sortEntries(classCounts)) {
|
|
116
|
-
console.log(`- ${name}: ${count}`);
|
|
117
|
-
}
|
|
118
|
-
console.log("Guards:");
|
|
119
|
-
for (const [name, count] of sortEntries(guardCounts)) {
|
|
120
|
-
console.log(`- ${name}: ${count}`);
|
|
121
|
-
}
|
|
122
|
-
NODE_EOF
|
|
123
|
-
)"
|
|
124
|
-
|
|
125
|
-
JSONL_FILE="$log_file" FILTER_RUN_ID="$filter_run_id" "$js_runtime" -e "$js_code"
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
|
+
exec "$SCRIPT_DIR/lib/run-forge-helper.sh" "summarize-failures.sh" "$@"
|
package/scripts/switch-plan.sh
CHANGED
|
@@ -1,153 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Select the current plan for this worktree.
|
|
3
|
-
# Other worktrees may hold different active-plan markers concurrently.
|
|
4
|
-
|
|
5
2
|
set -euo pipefail
|
|
6
|
-
|
|
7
3
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
-
|
|
9
|
-
cd "$REPO_ROOT"
|
|
10
|
-
elif [[ "$SCRIPT_DIR" == */.ai/harness/scripts ]]; then
|
|
11
|
-
cd "$SCRIPT_DIR/../../.."
|
|
12
|
-
else
|
|
13
|
-
cd "$SCRIPT_DIR/.."
|
|
14
|
-
fi
|
|
15
|
-
|
|
16
|
-
usage() {
|
|
17
|
-
cat <<'USAGE_EOF'
|
|
18
|
-
Usage: scripts/switch-plan.sh --plan <plan-file>
|
|
19
|
-
scripts/switch-plan.sh --list
|
|
20
|
-
|
|
21
|
-
Options:
|
|
22
|
-
--plan <path> Switch to the specified plan (saves current state, restores target)
|
|
23
|
-
--list List all non-archived plans with active marker
|
|
24
|
-
USAGE_EOF
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
extract_status() {
|
|
28
|
-
local file="$1"
|
|
29
|
-
awk '/\*\*Status\*\*:/ {sub(/^.*\*\*Status\*\*: */, ""); gsub(/\r/, ""); print; exit}' "$file" | xargs
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
ACTIVE_PLAN_MARKER=".ai/harness/active-plan"
|
|
33
|
-
LEGACY_ACTIVE_PLAN_MARKER=".claude/.active-plan"
|
|
34
|
-
ACTIVE_WORKTREE_MARKER=".ai/harness/active-worktree"
|
|
35
|
-
|
|
36
|
-
read_active_plan_marker() {
|
|
37
|
-
local marker_file="$1"
|
|
38
|
-
local marker_plan
|
|
39
|
-
|
|
40
|
-
if [[ -f "$marker_file" ]]; then
|
|
41
|
-
marker_plan="$(cat "$marker_file" 2>/dev/null | xargs)"
|
|
42
|
-
if [[ -n "$marker_plan" && -f "$marker_plan" ]]; then
|
|
43
|
-
printf '%s' "$marker_plan"
|
|
44
|
-
return 0
|
|
45
|
-
fi
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
|
-
return 1
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
get_active_plan() {
|
|
52
|
-
read_active_plan_marker "$ACTIVE_PLAN_MARKER" \
|
|
53
|
-
|| read_active_plan_marker "$LEGACY_ACTIVE_PLAN_MARKER"
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
write_active_plan_marker() {
|
|
57
|
-
local plan_file="$1"
|
|
58
|
-
mkdir -p "$(dirname "$ACTIVE_PLAN_MARKER")" "$(dirname "$LEGACY_ACTIVE_PLAN_MARKER")" "$(dirname "$ACTIVE_WORKTREE_MARKER")"
|
|
59
|
-
printf '%s' "$plan_file" > "$ACTIVE_PLAN_MARKER"
|
|
60
|
-
printf '%s' "$plan_file" > "$LEGACY_ACTIVE_PLAN_MARKER"
|
|
61
|
-
pwd -P > "$ACTIVE_WORKTREE_MARKER"
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
do_list() {
|
|
65
|
-
local active
|
|
66
|
-
active="$(get_active_plan || true)"
|
|
67
|
-
|
|
68
|
-
if [[ ! -d "plans" ]]; then
|
|
69
|
-
echo "No plans/ directory found."
|
|
70
|
-
return
|
|
71
|
-
fi
|
|
72
|
-
|
|
73
|
-
local found=0
|
|
74
|
-
while IFS= read -r plan; do
|
|
75
|
-
[[ -n "$plan" ]] || continue
|
|
76
|
-
found=1
|
|
77
|
-
local status marker
|
|
78
|
-
status="$(extract_status "$plan")"
|
|
79
|
-
status="${status:-(unknown)}"
|
|
80
|
-
if [[ "$plan" == "$active" ]]; then
|
|
81
|
-
marker="[*]"
|
|
82
|
-
else
|
|
83
|
-
marker=" "
|
|
84
|
-
fi
|
|
85
|
-
printf '%s %s Status: %s\n' "$marker" "$plan" "$status"
|
|
86
|
-
done < <(find plans -maxdepth 1 -type f -name 'plan-*.md' 2>/dev/null | sort)
|
|
87
|
-
|
|
88
|
-
if [[ "$found" -eq 0 ]]; then
|
|
89
|
-
echo "No plans found in plans/"
|
|
90
|
-
fi
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
do_switch() {
|
|
94
|
-
local target_plan="$1"
|
|
95
|
-
|
|
96
|
-
if [[ ! -f "$target_plan" ]]; then
|
|
97
|
-
echo "Error: plan file not found: $target_plan" >&2
|
|
98
|
-
exit 1
|
|
99
|
-
fi
|
|
100
|
-
|
|
101
|
-
local current_plan
|
|
102
|
-
current_plan="$(get_active_plan || true)"
|
|
103
|
-
|
|
104
|
-
if [[ "$current_plan" == "$target_plan" ]]; then
|
|
105
|
-
echo "[PlanSwitch] Already on $target_plan"
|
|
106
|
-
return 0
|
|
107
|
-
fi
|
|
108
|
-
|
|
109
|
-
write_active_plan_marker "$target_plan"
|
|
110
|
-
echo "[PlanSwitch] Selected $target_plan for worktree $(pwd -P)"
|
|
111
|
-
echo "[PlanSwitch] tasks/todos.md is a deferred-goal ledger and was left unchanged."
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
# --- Main ---
|
|
115
|
-
target_plan=""
|
|
116
|
-
mode=""
|
|
117
|
-
|
|
118
|
-
while [[ $# -gt 0 ]]; do
|
|
119
|
-
case "$1" in
|
|
120
|
-
--plan)
|
|
121
|
-
[[ -n "${2:-}" ]] || { echo "Error: --plan requires a value" >&2; usage; exit 2; }
|
|
122
|
-
target_plan="$2"
|
|
123
|
-
mode="switch"
|
|
124
|
-
shift 2
|
|
125
|
-
;;
|
|
126
|
-
--list)
|
|
127
|
-
mode="list"
|
|
128
|
-
shift
|
|
129
|
-
;;
|
|
130
|
-
--help|-h)
|
|
131
|
-
usage
|
|
132
|
-
exit 0
|
|
133
|
-
;;
|
|
134
|
-
*)
|
|
135
|
-
echo "Unknown argument: $1" >&2
|
|
136
|
-
usage
|
|
137
|
-
exit 2
|
|
138
|
-
;;
|
|
139
|
-
esac
|
|
140
|
-
done
|
|
141
|
-
|
|
142
|
-
case "$mode" in
|
|
143
|
-
list)
|
|
144
|
-
do_list
|
|
145
|
-
;;
|
|
146
|
-
switch)
|
|
147
|
-
do_switch "$target_plan"
|
|
148
|
-
;;
|
|
149
|
-
*)
|
|
150
|
-
usage
|
|
151
|
-
exit 2
|
|
152
|
-
;;
|
|
153
|
-
esac
|
|
4
|
+
exec "$SCRIPT_DIR/lib/run-forge-helper.sh" "switch-plan.sh" "$@"
|
|
@@ -1,381 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
cat <<'USAGE_EOF'
|
|
6
|
-
Usage: scripts/sync-brain-docs.sh [--manifest PATH] (--all | --changed PATH | --check) [--dry-run] [--require-vault]
|
|
7
|
-
|
|
8
|
-
Synchronizes explicitly opted-in repo docs to the local default brain file vault.
|
|
9
|
-
Only manifest entries with sync.direction=repo-to-brain are eligible.
|
|
10
|
-
USAGE_EOF
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
manifest_path=".ai/harness/brain-manifest.json"
|
|
14
|
-
mode_all=0
|
|
15
|
-
mode_check=0
|
|
16
|
-
dry_run=0
|
|
17
|
-
require_vault=0
|
|
18
|
-
changed_paths=()
|
|
19
|
-
|
|
20
|
-
while [[ $# -gt 0 ]]; do
|
|
21
|
-
case "$1" in
|
|
22
|
-
--manifest)
|
|
23
|
-
manifest_path="${2:-}"
|
|
24
|
-
shift 2
|
|
25
|
-
;;
|
|
26
|
-
--all)
|
|
27
|
-
mode_all=1
|
|
28
|
-
shift
|
|
29
|
-
;;
|
|
30
|
-
--changed)
|
|
31
|
-
changed_paths+=("${2:-}")
|
|
32
|
-
shift 2
|
|
33
|
-
;;
|
|
34
|
-
--check)
|
|
35
|
-
mode_check=1
|
|
36
|
-
shift
|
|
37
|
-
;;
|
|
38
|
-
--dry-run)
|
|
39
|
-
dry_run=1
|
|
40
|
-
shift
|
|
41
|
-
;;
|
|
42
|
-
--require-vault)
|
|
43
|
-
require_vault=1
|
|
44
|
-
shift
|
|
45
|
-
;;
|
|
46
|
-
--help|-h)
|
|
47
|
-
usage
|
|
48
|
-
exit 0
|
|
49
|
-
;;
|
|
50
|
-
*)
|
|
51
|
-
echo "sync-brain-docs: unknown argument: $1" >&2
|
|
52
|
-
usage >&2
|
|
53
|
-
exit 2
|
|
54
|
-
;;
|
|
55
|
-
esac
|
|
56
|
-
done
|
|
57
|
-
|
|
58
|
-
resolve_js_runtime() {
|
|
59
|
-
if command -v node >/dev/null 2>&1; then
|
|
60
|
-
printf 'node'
|
|
61
|
-
return 0
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
if command -v bun >/dev/null 2>&1; then
|
|
65
|
-
printf 'bun'
|
|
66
|
-
return 0
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
return 1
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
runtime="$(resolve_js_runtime || true)"
|
|
73
|
-
if [[ -z "$runtime" ]]; then
|
|
74
|
-
echo "[BrainSync] Missing node or bun to read brain manifest: $manifest_path" >&2
|
|
75
|
-
exit 1
|
|
76
|
-
fi
|
|
77
|
-
|
|
78
|
-
if [[ "$mode_all" -eq 0 && "$mode_check" -eq 0 && "${#changed_paths[@]}" -eq 0 ]]; then
|
|
79
|
-
echo "sync-brain-docs: choose --all, --changed PATH, or --check" >&2
|
|
80
|
-
usage >&2
|
|
81
|
-
exit 2
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
|
-
changed_json='[]'
|
|
85
|
-
if [[ "${#changed_paths[@]}" -gt 0 ]]; then
|
|
86
|
-
changed_json="$(
|
|
87
|
-
"$runtime" -e '
|
|
88
|
-
const values = process.argv.slice(1);
|
|
89
|
-
process.stdout.write(JSON.stringify(values));
|
|
90
|
-
' "${changed_paths[@]}"
|
|
91
|
-
)"
|
|
92
|
-
fi
|
|
93
|
-
|
|
94
|
-
"$runtime" - "$manifest_path" "$mode_all" "$mode_check" "$dry_run" "$require_vault" "$changed_json" <<'JS_EOF'
|
|
95
|
-
const fs = require("fs");
|
|
96
|
-
const path = require("path");
|
|
97
|
-
const os = require("os");
|
|
98
|
-
|
|
99
|
-
const [, , manifestArg, allArg, checkArg, dryRunArg, requireVaultArg, changedJson] = process.argv;
|
|
100
|
-
const repoRoot = fs.realpathSync(process.cwd());
|
|
101
|
-
const manifestPath = path.resolve(repoRoot, manifestArg || ".ai/harness/brain-manifest.json");
|
|
102
|
-
const modeAll = allArg === "1";
|
|
103
|
-
const modeCheck = checkArg === "1";
|
|
104
|
-
const dryRun = dryRunArg === "1";
|
|
105
|
-
const requireVault = requireVaultArg === "1";
|
|
106
|
-
const changedPaths = JSON.parse(changedJson || "[]").map(normalizeRepoPathInput).filter(Boolean);
|
|
107
|
-
let issues = 0;
|
|
108
|
-
let synced = 0;
|
|
109
|
-
let skipped = 0;
|
|
110
|
-
|
|
111
|
-
function issue(message) {
|
|
112
|
-
console.log(`[BrainSync] ${message}`);
|
|
113
|
-
issues += 1;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function warn(message) {
|
|
117
|
-
console.log(`[BrainSync] warning: ${message}`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function readJson(filePath, label) {
|
|
121
|
-
try {
|
|
122
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
123
|
-
} catch (_error) {
|
|
124
|
-
issue(`Cannot read ${label}: ${path.relative(repoRoot, filePath) || filePath}`);
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function normalizeSlashes(value) {
|
|
130
|
-
return String(value || "").replaceAll(path.sep, "/");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function isInside(root, candidate) {
|
|
134
|
-
const rel = path.relative(root, candidate);
|
|
135
|
-
return rel === "" || (!!rel && !rel.startsWith("..") && !path.isAbsolute(rel));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function realpathOrNull(filePath) {
|
|
139
|
-
try {
|
|
140
|
-
return fs.realpathSync(filePath);
|
|
141
|
-
} catch (_error) {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function firstExistingParent(filePath) {
|
|
147
|
-
let cursor = path.resolve(filePath);
|
|
148
|
-
const missingParts = [];
|
|
149
|
-
while (!fs.existsSync(cursor)) {
|
|
150
|
-
const parent = path.dirname(cursor);
|
|
151
|
-
if (parent === cursor) return null;
|
|
152
|
-
missingParts.unshift(path.basename(cursor));
|
|
153
|
-
cursor = parent;
|
|
154
|
-
}
|
|
155
|
-
return { parent: cursor, rest: missingParts.join(path.sep) };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function normalizeRepoPathInput(value) {
|
|
159
|
-
if (!value) return "";
|
|
160
|
-
const raw = String(value);
|
|
161
|
-
const absolute = path.isAbsolute(raw) ? raw : path.resolve(repoRoot, raw);
|
|
162
|
-
const real = realpathOrNull(absolute);
|
|
163
|
-
const rel = path.relative(repoRoot, real || absolute);
|
|
164
|
-
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
165
|
-
return "";
|
|
166
|
-
}
|
|
167
|
-
return normalizeSlashes(rel);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function safeRepoPath(value, label, id) {
|
|
171
|
-
const raw = String(value || "");
|
|
172
|
-
if (!raw || raw.includes("\n") || raw.includes("\r") || path.isAbsolute(raw)) {
|
|
173
|
-
issue(`Entry ${id} has invalid ${label}: ${raw || "(empty)"}`);
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
const normalized = path.normalize(raw);
|
|
177
|
-
if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) {
|
|
178
|
-
issue(`Entry ${id} ${label} escapes repo: ${raw}`);
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
return normalizeSlashes(normalized);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function stripWildcard(logicalPath) {
|
|
185
|
-
return String(logicalPath || "").replace(/\/\*$/, "/");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const brainRoot = process.env.FORGE_BRAIN_ROOT ||
|
|
189
|
-
path.join(os.homedir(), "Library", "Mobile Documents", "com~apple~CloudDocs", "brain");
|
|
190
|
-
|
|
191
|
-
function logicalToLocal(logicalPath, id) {
|
|
192
|
-
const value = String(logicalPath || "");
|
|
193
|
-
if (!value.startsWith("brain/")) {
|
|
194
|
-
issue(`Entry ${id} brain_path must start with brain/: ${value || "(empty)"}`);
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
const rel = value.slice("brain/".length);
|
|
198
|
-
if (!rel || rel.includes("\n") || rel.includes("\r")) {
|
|
199
|
-
issue(`Entry ${id} has invalid brain_path: ${value || "(empty)"}`);
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
const local = path.resolve(brainRoot, rel);
|
|
203
|
-
const root = path.resolve(brainRoot);
|
|
204
|
-
if (!isInside(root, local)) {
|
|
205
|
-
issue(`Entry ${id} brain_path escapes brain root: ${value}`);
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
return local;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function validateSourceInsideRepo(sourceFile, sourcePath, id) {
|
|
212
|
-
if (!isInside(repoRoot, sourceFile)) {
|
|
213
|
-
issue(`Entry ${id} source file escapes repo: ${sourcePath}`);
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
const real = realpathOrNull(sourceFile);
|
|
217
|
-
if (real && !isInside(repoRoot, real)) {
|
|
218
|
-
issue(`Entry ${id} source file symlink escapes repo: ${sourcePath}`);
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function validateTargetInsideBrainRoot(targetPath, brainPath, id) {
|
|
225
|
-
const brainRootReal = realpathOrNull(brainRoot);
|
|
226
|
-
if (!brainRootReal) return true;
|
|
227
|
-
|
|
228
|
-
const targetReal = realpathOrNull(targetPath);
|
|
229
|
-
if (targetReal && !isInside(brainRootReal, targetReal)) {
|
|
230
|
-
issue(`Entry ${id} brain file symlink escapes brain root: ${brainPath}`);
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const parentInfo = firstExistingParent(targetPath);
|
|
235
|
-
if (parentInfo) {
|
|
236
|
-
const parentReal = realpathOrNull(parentInfo.parent);
|
|
237
|
-
const effectiveTarget = parentReal
|
|
238
|
-
? path.resolve(parentReal, parentInfo.rest)
|
|
239
|
-
: path.resolve(targetPath);
|
|
240
|
-
if (!isInside(brainRootReal, effectiveTarget)) {
|
|
241
|
-
issue(`Entry ${id} brain path escapes brain root through symlink: ${brainPath}`);
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function syncConfig(entry) {
|
|
250
|
-
const sync = entry.sync && typeof entry.sync === "object" ? entry.sync : {};
|
|
251
|
-
const direction = sync.direction || entry.sync_direction || "";
|
|
252
|
-
if (direction !== "repo-to-brain") return null;
|
|
253
|
-
if (sync.enabled === false || entry.sync_enabled === false) return null;
|
|
254
|
-
const id = entry.id || "(missing id)";
|
|
255
|
-
return {
|
|
256
|
-
id,
|
|
257
|
-
sourcePath: sync.source_path || entry.source_path || entry.repo_path,
|
|
258
|
-
brainPath: sync.brain_path || entry.brain_path,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (!fs.existsSync(manifestPath)) {
|
|
263
|
-
issue(`Missing brain manifest: ${path.relative(repoRoot, manifestPath) || manifestPath}`);
|
|
264
|
-
process.exit(1);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const manifest = readJson(manifestPath, "brain manifest");
|
|
268
|
-
if (!manifest) process.exit(1);
|
|
269
|
-
|
|
270
|
-
const entries = Array.isArray(manifest.entries) ? manifest.entries : [];
|
|
271
|
-
const defaultPrefix = stripWildcard(manifest.default_brain_path);
|
|
272
|
-
const eligible = [];
|
|
273
|
-
|
|
274
|
-
for (const entry of entries) {
|
|
275
|
-
const config = syncConfig(entry);
|
|
276
|
-
if (!config) continue;
|
|
277
|
-
const sourcePath = safeRepoPath(config.sourcePath, "sync source_path", config.id);
|
|
278
|
-
const brainPath = String(config.brainPath || "");
|
|
279
|
-
if (brainPath && defaultPrefix && !brainPath.startsWith(defaultPrefix)) {
|
|
280
|
-
issue(`Entry ${config.id} brain_path is outside default_brain_path: ${brainPath}`);
|
|
281
|
-
}
|
|
282
|
-
const targetPath = logicalToLocal(brainPath, config.id);
|
|
283
|
-
if (!sourcePath || !targetPath) continue;
|
|
284
|
-
const sourceFile = path.resolve(repoRoot, sourcePath);
|
|
285
|
-
if (!validateSourceInsideRepo(sourceFile, sourcePath, config.id)) continue;
|
|
286
|
-
eligible.push({ id: config.id, sourcePath, sourceFile, brainPath, targetPath });
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const selected = eligible.filter((entry) => {
|
|
290
|
-
if (modeAll || modeCheck) return true;
|
|
291
|
-
return changedPaths.includes(entry.sourcePath);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
if (selected.length > 0 && !modeCheck && !fs.existsSync(brainRoot)) {
|
|
295
|
-
issue(`vault root unavailable: ${brainRoot}`);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (selected.length > 0 && modeCheck && !fs.existsSync(brainRoot)) {
|
|
299
|
-
const message = `vault root unavailable; skipped sync drift checks: ${brainRoot}`;
|
|
300
|
-
if (requireVault) issue(message);
|
|
301
|
-
else warn(message);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
for (const entry of selected) {
|
|
305
|
-
if (!fs.existsSync(entry.sourceFile)) {
|
|
306
|
-
issue(`Entry ${entry.id} source file is missing: ${entry.sourcePath}`);
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (!fs.existsSync(brainRoot)) {
|
|
311
|
-
skipped += 1;
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (!validateTargetInsideBrainRoot(entry.targetPath, entry.brainPath, entry.id)) {
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
let sourceContent;
|
|
320
|
-
try {
|
|
321
|
-
sourceContent = fs.readFileSync(entry.sourceFile, "utf8");
|
|
322
|
-
} catch (error) {
|
|
323
|
-
const code = error && error.code ? ` (${error.code})` : "";
|
|
324
|
-
issue(`Entry ${entry.id} source file is unreadable: ${entry.sourcePath}${code}`);
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const targetExists = fs.existsSync(entry.targetPath);
|
|
329
|
-
let targetContent = null;
|
|
330
|
-
if (targetExists) {
|
|
331
|
-
try {
|
|
332
|
-
targetContent = fs.readFileSync(entry.targetPath, "utf8");
|
|
333
|
-
} catch (error) {
|
|
334
|
-
const code = error && error.code ? ` (${error.code})` : "";
|
|
335
|
-
const message = `Entry ${entry.id} brain file is unreadable: ${entry.brainPath}${code}`;
|
|
336
|
-
if (modeCheck && !requireVault) {
|
|
337
|
-
warn(`${message}; skipped sync drift check`);
|
|
338
|
-
skipped += 1;
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
issue(message);
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (modeCheck) {
|
|
347
|
-
if (!targetExists) {
|
|
348
|
-
issue(`Entry ${entry.id} brain file is missing: ${entry.brainPath}`);
|
|
349
|
-
} else if (targetContent !== sourceContent) {
|
|
350
|
-
issue(`Entry ${entry.id} brain file differs from source: ${entry.sourcePath} -> ${entry.brainPath}`);
|
|
351
|
-
}
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (targetExists && targetContent === sourceContent) {
|
|
356
|
-
skipped += 1;
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (dryRun) {
|
|
361
|
-
console.log(`[BrainSync] would sync ${entry.sourcePath} -> ${entry.brainPath}`);
|
|
362
|
-
synced += 1;
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
fs.mkdirSync(path.dirname(entry.targetPath), { recursive: true });
|
|
367
|
-
fs.writeFileSync(entry.targetPath, sourceContent);
|
|
368
|
-
console.log(`[BrainSync] synced ${entry.sourcePath} -> ${entry.brainPath}`);
|
|
369
|
-
synced += 1;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (issues > 0) process.exit(1);
|
|
373
|
-
|
|
374
|
-
if (selected.length === 0 && (modeAll || modeCheck)) {
|
|
375
|
-
console.log("[BrainSync] no repo-to-brain entries");
|
|
376
|
-
} else if (synced === 0 && selected.length > 0 && !modeCheck) {
|
|
377
|
-
console.log(`[BrainSync] up to date (${skipped} checked)`);
|
|
378
|
-
} else if (modeCheck && selected.length > 0) {
|
|
379
|
-
console.log("[BrainSync] OK");
|
|
380
|
-
}
|
|
381
|
-
JS_EOF
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
|
+
exec "$SCRIPT_DIR/lib/run-forge-helper.sh" "sync-brain-docs.sh" "$@"
|