@moretea-labs/forge 1.7.0 → 1.7.2

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 (175) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +26 -0
  3. package/CLAUDE.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +1 -1
  5. package/agents/openai.yaml +1 -1
  6. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  7. package/assets/plugin-registry.v1.json +3 -3
  8. package/assets/skill-version.json +2 -2
  9. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  10. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  11. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  12. package/assets/templates/helpers/workflow-contract.ts +5 -1
  13. package/docs/architecture/CURRENT.md +12 -4
  14. package/docs/forge-plugin-management.md +1 -1
  15. package/docs/operations/controller-performance-and-502.md +19 -0
  16. package/docs/operations/homebrew.md +2 -2
  17. package/package.json +5 -2
  18. package/scripts/architecture-event.ts +2 -820
  19. package/scripts/architecture-queue.sh +2 -645
  20. package/scripts/archive-architecture-request.sh +2 -242
  21. package/scripts/archive-workflow.sh +1 -291
  22. package/scripts/benchmark-work-efficiency.ts +0 -0
  23. package/scripts/capability-config.ts +2 -392
  24. package/scripts/capability-resolver.ts +2 -503
  25. package/scripts/capture-plan.sh +1 -332
  26. package/scripts/check-agent-tooling.sh +2 -1531
  27. package/scripts/check-architecture-sync.sh +2 -302
  28. package/scripts/check-brain-manifest.sh +2 -253
  29. package/scripts/check-context-files.sh +2 -47
  30. package/scripts/check-deploy-sql-order.sh +2 -115
  31. package/scripts/check-runtime-architecture.mjs +243 -16
  32. package/scripts/check-skill-version.ts +3 -191
  33. package/scripts/check-source-duplication.mjs +61 -0
  34. package/scripts/check-stable-baseline.ts +0 -0
  35. package/scripts/check-task-sync.sh +2 -63
  36. package/scripts/check-task-workflow.sh +2 -1000
  37. package/scripts/codex-handoff-resume.sh +2 -267
  38. package/scripts/context-contract-sync.sh +2 -514
  39. package/scripts/contract-run.ts +2 -463
  40. package/scripts/contract-worktree.sh +1 -702
  41. package/scripts/ensure-task-workflow.sh +1 -1192
  42. package/scripts/inspect-project-state.ts +4 -279
  43. package/scripts/lib/project-init-lib.sh +7 -2
  44. package/scripts/lib/run-forge-helper.sh +19 -0
  45. package/scripts/lib/run-forge-helper.ts +26 -0
  46. package/scripts/migrate-project-template.sh +1 -33
  47. package/scripts/migrate-workflow-docs.ts +4 -412
  48. package/scripts/new-plan.sh +1 -180
  49. package/scripts/new-spec.sh +1 -47
  50. package/scripts/new-sprint.sh +1 -46
  51. package/scripts/plan-to-todo.sh +1 -771
  52. package/scripts/prepare-codex-handoff.sh +2 -201
  53. package/scripts/prepare-handoff.sh +1 -78
  54. package/scripts/refresh-current-status.sh +2 -467
  55. package/scripts/run-governed-gate.ts +3 -0
  56. package/scripts/select-agent-context-blocks.sh +2 -64
  57. package/scripts/ship-worktrees.sh +1 -531
  58. package/scripts/sprint-backlog.sh +1 -834
  59. package/scripts/stage-runtime-release.ts +2 -1
  60. package/scripts/summarize-failures.sh +2 -123
  61. package/scripts/switch-plan.sh +1 -150
  62. package/scripts/sync-brain-docs.sh +2 -379
  63. package/scripts/sync-generated-authority.mjs +175 -0
  64. package/scripts/verify-contract.sh +2 -593
  65. package/scripts/verify-sprint.sh +1 -500
  66. package/scripts/workflow-contract.ts +4 -201
  67. package/scripts/workstream-sync.sh +2 -261
  68. package/src/cli/agent-jobs/executor-health.ts +1 -1
  69. package/src/cli/commands/plugin.ts +12 -3
  70. package/src/cli/controller/check-runner.ts +7 -2
  71. package/src/cli/controller/context-pack.ts +46 -21
  72. package/src/cli/github/contracts.ts +16 -0
  73. package/src/cli/github/github.ts +2 -16
  74. package/src/cli/github/session.ts +2 -1
  75. package/src/cli/local-bridge/facade-api.ts +37 -47
  76. package/src/cli/local-bridge/server.ts +2 -2
  77. package/src/cli/mcp/legacy-tool-service.ts +10 -33
  78. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  79. package/src/cli/mcp/repository-tools.ts +125 -101
  80. package/src/cli/mcp/server.ts +57 -13
  81. package/src/cli/mcp/tool-contract.ts +14 -0
  82. package/src/cli/mcp/toolset.ts +2 -2
  83. package/src/cli/mcp/transports/http.ts +23 -0
  84. package/src/cli/repositories/command-classifier.ts +2 -2
  85. package/src/cli/repositories/command-executor.ts +24 -6
  86. package/src/cli/repositories/repository-snapshot.ts +5 -2
  87. package/src/cli/repositories/structured-git.ts +57 -3
  88. package/src/cli/repository/inspector.ts +11 -3
  89. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  90. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  91. package/src/runtime/context/semantic-navigation.ts +9 -61
  92. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +69 -29
  93. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  94. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  95. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  96. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  97. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  98. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  99. package/src/runtime/control-plane/execution/work-handle-authority.ts +150 -0
  100. package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
  101. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  102. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  103. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  104. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  105. package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
  106. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
  107. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  108. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  109. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  110. package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
  111. package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
  112. package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
  113. package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
  114. package/src/runtime/control-plane/facade/index.ts +2 -0
  115. package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
  116. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  117. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  118. package/src/runtime/control-plane/facade/types.ts +68 -5
  119. package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
  120. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  121. package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
  122. package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
  123. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
  124. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
  125. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
  126. package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
  127. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  128. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  129. package/src/runtime/control-plane/routing/route-policy.ts +18 -6
  130. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  131. package/src/runtime/evidence/process-check-execution.ts +18 -0
  132. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  133. package/src/runtime/execution/process-runtime/check-result.ts +53 -18
  134. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  135. package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
  136. package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
  137. package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
  138. package/src/runtime/execution/process-runtime/runtime.ts +10 -0
  139. package/src/runtime/execution/process-runtime/types.ts +2 -12
  140. package/src/runtime/execution/work-activity.ts +21 -0
  141. package/src/runtime/execution/workers/executor.ts +18 -13
  142. package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
  143. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  144. package/src/runtime/gateway/mcp/router.ts +2 -2
  145. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
  146. package/src/runtime/gateway/mcp/runtime-tools.ts +815 -1015
  147. package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
  148. package/src/runtime/plugins/browser-adapter.ts +256 -1083
  149. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  150. package/src/runtime/plugins/browser-handoff.ts +18 -3
  151. package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
  152. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  153. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  154. package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
  155. package/src/runtime/plugins/browser-runtime.ts +10 -0
  156. package/src/runtime/plugins/browser-session-store.ts +121 -0
  157. package/src/runtime/plugins/browser-session-types.ts +98 -0
  158. package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
  159. package/src/runtime/plugins/external-adapter.ts +92 -58
  160. package/src/runtime/plugins/macos-capability-broker.ts +0 -8
  161. package/src/runtime/plugins/store.ts +66 -9
  162. package/src/runtime/plugins/types.ts +3 -1
  163. package/src/runtime/recovery/maintenance-executor.ts +61 -25
  164. package/src/runtime/root/package-connector-service.ts +46 -4
  165. package/src/runtime/root/package-runtime-release.ts +3 -2
  166. package/src/runtime/root/package-runtime-service.ts +48 -3
  167. package/src/runtime/root/release-manifest.ts +137 -16
  168. package/src/runtime/root/release-materialize.ts +109 -35
  169. package/src/runtime/root/types.ts +10 -0
  170. package/src/runtime/standalone-recovery/core.ts +425 -194
  171. package/src/runtime/standalone-recovery/entry.ts +1 -1
  172. package/src/runtime/standalone-recovery/installer.ts +1 -1
  173. package/src/runtime/workflow/schedules/engine.ts +113 -87
  174. package/src/runtime/workflow/schedules/settlement.ts +26 -23
  175. package/src/runtime/workflow/schedules/store.ts +79 -10
@@ -1,202 +1,5 @@
1
- import { existsSync, readFileSync } from "fs";
2
- import { dirname, join } from "path";
3
- import { fileURLToPath } from "url";
1
+ #!/usr/bin/env bun
2
+ export * from "../assets/templates/helpers/workflow-contract";
3
+ import { runTrackedForgeHelper } from "./lib/run-forge-helper";
4
4
 
5
- export type WorkflowContract = {
6
- version: string;
7
- contractId: string;
8
- compatibility: {
9
- agents: string[];
10
- repoLocalFirst: boolean;
11
- };
12
- externalTooling?: {
13
- waza?: {
14
- sourceRepo: string;
15
- managedSkills: string[];
16
- primaryHost: string;
17
- codexPrimaryPath: string;
18
- stagingCachePath: string;
19
- syncMode: string;
20
- hostDriftPolicy: string;
21
- };
22
- codexAutomationProfile?: {
23
- requiredSkills: string[];
24
- optionalSkills: string[];
25
- mode: string;
26
- source: string;
27
- routes: {
28
- workflowHealth: string;
29
- reviewGate: string;
30
- architectureDiagram: string;
31
- };
32
- vendoringPolicy: string;
33
- };
34
- diagramDesign?: {
35
- skillName: string;
36
- primaryHost: string;
37
- codexPrimaryPath: string;
38
- syncMode: string;
39
- vendoringPolicy: string;
40
- };
41
- };
42
- agenticDevelopment?: {
43
- routing: {
44
- productDiscovery: string;
45
- complexEngineeringPlan: string;
46
- designPlan: string;
47
- smallOrMediumPlan: string;
48
- bugOrRegression: string;
49
- postImplementationReview: string;
50
- };
51
- dueDiligence: {
52
- levels: string[];
53
- explicitReportRequiredFor: string[];
54
- };
55
- };
56
- documentation?: {
57
- referenceConfigs?: {
58
- source: string;
59
- repoStubDirectory: string;
60
- packageDirectory: string;
61
- resolverCommand: string;
62
- stubMarker: string;
63
- };
64
- };
65
- helpers: {
66
- runtimeDirectory?: string;
67
- compatibilityDirectory?: string;
68
- scripts: string[];
69
- };
70
- artifacts: {
71
- runtimeManifest: string;
72
- requiredDirectories: string[];
73
- requiredFiles: string[];
74
- runtimeFiles?: string[];
75
- };
76
- documents: {
77
- spec: string;
78
- currentStatus?: string;
79
- planDirectory: string;
80
- taskChecklist?: string;
81
- deferredGoalLedger?: string;
82
- researchReportsDirectory: string;
83
- lessonsLog: string;
84
- };
85
- adoptionTemplates?: {
86
- files?: Record<
87
- string,
88
- {
89
- document: string;
90
- reason: string;
91
- lines: string[];
92
- }
93
- >;
94
- };
95
- migrations: {
96
- legacyVersions: string[];
97
- legacyPaths: string[];
98
- upgrade?: {
99
- strategyVersion: number;
100
- supportedLegacyVersions: string[];
101
- actionClasses: string[];
102
- safety: {
103
- removeOnlyOwnership: string;
104
- unknownFiles: string;
105
- customHooks: string;
106
- ignoredReferenceMaterial: string;
107
- localSecrets: string;
108
- };
109
- actions: Array<{
110
- id: string;
111
- signal: string;
112
- action: "preserve" | "archive" | "reconfigure" | "remove";
113
- risk: "low" | "medium" | "high";
114
- ownership: "known_generated" | "managed_config" | "user_authored" | "user_local";
115
- paths: string[];
116
- targetPaths?: string[];
117
- cleanupMode?: "always" | "generated_helper";
118
- summary: string;
119
- }>;
120
- };
121
- };
122
- };
123
-
124
- const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
125
- const REPO_ROOT = SCRIPT_DIR.endsWith("/.ai/harness/scripts")
126
- ? join(SCRIPT_DIR, "../../..")
127
- : SCRIPT_DIR.endsWith("/assets/templates/helpers")
128
- ? join(SCRIPT_DIR, "../../..")
129
- : join(SCRIPT_DIR, "..");
130
- const LOCAL_ASSET_PATH = join(REPO_ROOT, "assets", "workflow-contract.v1.json");
131
-
132
- export function resolveAgenticDevRoot(_repoRoot = REPO_ROOT): string {
133
- const configuredRoot =
134
- process.env.AGENTIC_DEV_ROOT ||
135
- process.env.AGENTIC_DEV_SKILL_ROOT;
136
- if (configuredRoot && configuredRoot.length > 0) return configuredRoot;
137
-
138
- if (existsSync(LOCAL_ASSET_PATH)) return REPO_ROOT;
139
-
140
- const home = process.env.HOME;
141
- if (home && home.length > 0) {
142
- const candidates = [
143
- join(home, "Projects", "forge"),
144
- join(home, ".codex", "skills", "forge"),
145
- join(home, ".claude", "skills", "forge"),
146
- join(home, ".agents", "skills", "forge"),
147
- ];
148
-
149
- for (const candidate of candidates) {
150
- if (existsSync(candidate)) return candidate;
151
- }
152
-
153
- return candidates[0];
154
- }
155
-
156
- return join(".forge", "skills", "forge");
157
- }
158
-
159
- export function resolveAgenticDevSkillRoot(repoRoot = REPO_ROOT): string {
160
- return resolveAgenticDevRoot(repoRoot);
161
- }
162
-
163
- export function resolveUpstreamWorkflowContract(repoRoot = REPO_ROOT): string {
164
- if (existsSync(LOCAL_ASSET_PATH)) return LOCAL_ASSET_PATH;
165
- return join(resolveAgenticDevRoot(repoRoot), "assets", "workflow-contract.v1.json");
166
- }
167
-
168
- export function loadWorkflowContract(
169
- contractPath = resolveUpstreamWorkflowContract()
170
- ): WorkflowContract {
171
- return JSON.parse(readFileSync(contractPath, "utf-8")) as WorkflowContract;
172
- }
173
-
174
- export function resolveInstalledWorkflowContract(repoRoot: string): string {
175
- return join(repoRoot, ".ai", "harness", "workflow-contract.json");
176
- }
177
-
178
- export function resolveWorkflowContractForRepo(repoRoot: string): string {
179
- const installedPath = resolveInstalledWorkflowContract(repoRoot);
180
- return existsSync(installedPath) ? installedPath : resolveUpstreamWorkflowContract(repoRoot);
181
- }
182
-
183
- export function getHelperScripts(contract: WorkflowContract): string[] {
184
- return [...contract.helpers.scripts];
185
- }
186
-
187
- export function getHelperRuntimeDir(contract: WorkflowContract): string {
188
- return contract.helpers.runtimeDirectory ?? contract.helpers.compatibilityDirectory ?? "scripts";
189
- }
190
-
191
- export function getRequiredDirectories(contract: WorkflowContract): string[] {
192
- return [...contract.artifacts.requiredDirectories];
193
- }
194
-
195
- export function getRequiredFiles(contract: WorkflowContract): string[] {
196
- return [...contract.artifacts.requiredFiles];
197
- }
198
-
199
- if (import.meta.main) {
200
- const contract = loadWorkflowContract(process.argv[2] || resolveUpstreamWorkflowContract());
201
- console.log(JSON.stringify(contract, null, 2));
202
- }
5
+ if (import.meta.main) runTrackedForgeHelper("workflow-contract.ts");
@@ -1,263 +1,4 @@
1
1
  #!/bin/bash
2
2
  set -euo pipefail
3
-
4
- # Contract invariant: durable capability ledgers live under tasks/workstreams.
5
-
6
- usage() {
7
- cat <<'USAGE_EOF'
8
- Usage:
9
- scripts/workstream-sync.sh ensure --block <capability-prefix> [--slug <slug>] [--title <title>] [--plan <plan-file>] [--slice <todo-id>] [--request <architecture-request>]
10
- USAGE_EOF
11
- }
12
-
13
- repo="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
14
- repo="$(cd "$repo" && pwd)"
15
- cd "$repo"
16
-
17
- command_name="${1:-ensure}"
18
- shift || true
19
-
20
- functional_block=""
21
- slug=""
22
- title=""
23
- source_plan="(none)"
24
- current_slice="todo-01"
25
- request_file="(none)"
26
-
27
- json_escape() {
28
- local value="$1"
29
- value="${value//\\/\\\\}"
30
- value="${value//\"/\\\"}"
31
- value="${value//$'\n'/\\n}"
32
- value="${value//$'\r'/\\r}"
33
- value="${value//$'\t'/\\t}"
34
- printf '%s' "$value"
35
- }
36
-
37
- json_get() {
38
- local json_input="$1"
39
- local key="$2"
40
- local parsed=""
41
-
42
- if [[ -z "$json_input" ]]; then
43
- return 1
44
- fi
45
-
46
- if command -v jq >/dev/null 2>&1; then
47
- parsed="$(printf '%s' "$json_input" | jq -r ".$key // empty" 2>/dev/null || true)"
48
- fi
49
-
50
- if [[ -z "$parsed" ]] && command -v node >/dev/null 2>&1; then
51
- parsed="$(JSON_INPUT="$json_input" JSON_KEY="$key" node -e '
52
- const raw = process.env.JSON_INPUT || "";
53
- const key = process.env.JSON_KEY || "";
54
- try {
55
- const value = JSON.parse(raw)[key];
56
- if (value === undefined || value === null) process.exit(1);
57
- process.stdout.write(String(value));
58
- } catch {
59
- process.exit(1);
60
- }
61
- ' 2>/dev/null || true)"
62
- fi
63
-
64
- [[ -n "$parsed" ]] || return 1
65
- printf '%s' "$parsed"
66
- }
67
-
68
- safe_token() {
69
- local value="$1"
70
- value="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')"
71
- value="$(printf '%s' "$value" | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
72
- printf '%s' "${value:-workstream}"
73
- }
74
-
75
- validate_block() {
76
- local block="$1"
77
- case "$block" in
78
- ""|.|..|../*|*/../*|/*|*$'\n'*|*$'\r'*)
79
- return 1
80
- ;;
81
- esac
82
- [[ -e "$block" ]]
83
- }
84
-
85
- while [[ $# -gt 0 ]]; do
86
- case "$1" in
87
- --block)
88
- [[ -n "${2:-}" ]] || { echo "workstream-sync: --block requires a value" >&2; exit 2; }
89
- functional_block="${2#./}"
90
- functional_block="${functional_block%/}"
91
- shift 2
92
- ;;
93
- --slug)
94
- [[ -n "${2:-}" ]] || { echo "workstream-sync: --slug requires a value" >&2; exit 2; }
95
- slug="$(safe_token "$2")"
96
- shift 2
97
- ;;
98
- --title)
99
- [[ -n "${2:-}" ]] || { echo "workstream-sync: --title requires a value" >&2; exit 2; }
100
- title="$2"
101
- shift 2
102
- ;;
103
- --plan)
104
- [[ -n "${2:-}" ]] || { echo "workstream-sync: --plan requires a value" >&2; exit 2; }
105
- source_plan="$2"
106
- shift 2
107
- ;;
108
- --slice)
109
- [[ -n "${2:-}" ]] || { echo "workstream-sync: --slice requires a value" >&2; exit 2; }
110
- current_slice="$2"
111
- shift 2
112
- ;;
113
- --request)
114
- [[ -n "${2:-}" ]] || { echo "workstream-sync: --request requires a value" >&2; exit 2; }
115
- request_file="$2"
116
- shift 2
117
- ;;
118
- --help|-h)
119
- usage
120
- exit 0
121
- ;;
122
- *)
123
- echo "workstream-sync: unknown argument: $1" >&2
124
- usage
125
- exit 2
126
- ;;
127
- esac
128
- done
129
-
130
- if [[ "$command_name" != "ensure" ]]; then
131
- echo "workstream-sync: unknown command: $command_name" >&2
132
- usage
133
- exit 2
134
- fi
135
-
136
- if ! validate_block "$functional_block"; then
137
- echo "workstream-sync: --block must be an existing repo-relative capability prefix" >&2
138
- exit 2
139
- fi
140
-
141
- if [[ ! -f "scripts/capability-resolver.ts" ]] || ! command -v bun >/dev/null 2>&1; then
142
- echo "workstream-sync: scripts/capability-resolver.ts and bun are required" >&2
143
- exit 2
144
- fi
145
-
146
- if ! resolver_json="$(bun scripts/capability-resolver.ts match --path "$functional_block" --format json 2>&1)"; then
147
- echo "$resolver_json" >&2
148
- exit 1
149
- fi
150
-
151
- if [[ "$(json_get "$resolver_json" "matched" || true)" != "true" ]]; then
152
- echo "workstream-sync: --block must match a declared capability prefix in .ai/context/capabilities.json" >&2
153
- exit 2
154
- fi
155
-
156
- capability_id="$(json_get "$resolver_json" "capability_id")"
157
- matched_prefix="$(json_get "$resolver_json" "matched_prefix")"
158
- architecture_domain="$(json_get "$resolver_json" "architecture_domain")"
159
- architecture_capability="$(json_get "$resolver_json" "architecture_capability")"
160
- architecture_module="$(json_get "$resolver_json" "architecture_module")"
161
- workstream_dir="$(json_get "$resolver_json" "workstream_dir")"
162
- contract_agents="$(json_get "$resolver_json" "contract_agents" || true)"
163
- contract_claude="$(json_get "$resolver_json" "contract_claude" || true)"
164
-
165
- if [[ -z "$slug" ]]; then
166
- if [[ -n "$title" ]]; then
167
- slug="$(safe_token "$title")"
168
- else
169
- slug="$(date '+%Y%m%d')-${architecture_capability}"
170
- fi
171
- fi
172
- title="${title:-$slug}"
173
- workstream_file="${workstream_dir}/${slug}.md"
174
- event_file=".ai/harness/events.jsonl"
175
- iso_timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')"
176
- architecture_domain_file="docs/architecture/domains/${architecture_domain}.md"
177
-
178
- mkdir -p "$workstream_dir" "$(dirname "$architecture_module")" "$(dirname "$architecture_domain_file")" ".ai/harness"
179
-
180
- if [[ ! -f "$architecture_domain_file" ]]; then
181
- cat > "$architecture_domain_file" <<EOF_DOMAIN
182
- # Architecture Domain: ${architecture_domain}
183
-
184
- > **Source**: \`.ai/context/capabilities.json\`
185
-
186
- ## Capabilities
187
-
188
- - \`${capability_id}\` -> \`${architecture_module}\`
189
- EOF_DOMAIN
190
- elif ! grep -Fq "$architecture_module" "$architecture_domain_file"; then
191
- {
192
- echo ""
193
- echo "- \`${capability_id}\` -> \`${architecture_module}\`"
194
- } >> "$architecture_domain_file"
195
- fi
196
-
197
- if [[ ! -f "$architecture_module" ]]; then
198
- cat > "$architecture_module" <<EOF_MODULE
199
- # Architecture Module: ${architecture_domain}/${architecture_capability}
200
-
201
- > **Capability ID**: \`${capability_id}\`
202
- > **Functional Block**: \`${functional_block}\`
203
- > **Matched Prefix**: \`${matched_prefix}\`
204
- > **Domain**: \`${architecture_domain}\`
205
- > **Capability**: \`${architecture_capability}\`
206
-
207
- ## Boundary
208
-
209
- - Responsibility: Keep the capability boundary, entrypoints, runtime path, dependency rules, and local verification pointers for \`${capability_id}\`.
210
- - Entrypoints: \`${matched_prefix}\`
211
- - Runtime path: \`${matched_prefix}\`
212
- - Local contracts: \`${contract_agents:-none}\`, \`${contract_claude:-none}\`
213
-
214
- ## Active Workstreams
215
-
216
- - \`${workstream_file}\`
217
- EOF_MODULE
218
- elif ! grep -Fq "$workstream_file" "$architecture_module"; then
219
- {
220
- echo ""
221
- echo "- \`${workstream_file}\`"
222
- } >> "$architecture_module"
223
- fi
224
-
225
- if [[ ! -f "$workstream_file" ]]; then
226
- cat > "$workstream_file" <<EOF_WORKSTREAM
227
- # Workstream: ${title}
228
-
229
- > **Status**: active
230
- > **Capability ID**: \`${capability_id}\`
231
- > **Functional Block**: \`${functional_block}\`
232
- > **Matched Prefix**: \`${matched_prefix}\`
233
- > **Architecture Domain**: \`${architecture_domain}\`
234
- > **Architecture Capability**: \`${architecture_capability}\`
235
- > **Architecture Module**: \`${architecture_module}\`
236
- > **Source Plan**: ${source_plan}
237
- > **Current Slice**: ${current_slice}
238
- > **Last Handoff**: \`.ai/harness/session/continuation.md\`
239
- > **Architecture Request**: ${request_file}
240
-
241
- ## Purpose
242
-
243
- Track durable multi-session progress for \`${capability_id}\` without inflating local agent instructions.
244
-
245
- ## TODOs
246
-
247
- - [ ] ${current_slice}: Complete the current executable slice for \`${capability_id}\`.
248
-
249
- ## Notes
250
-
251
- - Project the current slice into \`tasks/todos.md\` for a single session.
252
- - Keep architecture facts in \`${architecture_module}\`; keep execution progress here.
253
- EOF_WORKSTREAM
254
- fi
255
-
256
- event_json="{\"ts\":\"$(json_escape "$iso_timestamp")\",\"file_path\":\"$(json_escape "$workstream_file")\",\"severity\":\"medium\",\"functional_block\":\"$(json_escape "$functional_block")\",\"capability_id\":\"$(json_escape "$capability_id")\",\"matched_prefix\":\"$(json_escape "$matched_prefix")\",\"architecture_domain\":\"$(json_escape "$architecture_domain")\",\"architecture_capability\":\"$(json_escape "$architecture_capability")\",\"architecture_module\":\"$(json_escape "$architecture_module")\",\"workstream_dir\":\"$(json_escape "$workstream_dir")\",\"contract_agents\":\"$(json_escape "$contract_agents")\",\"contract_claude\":\"$(json_escape "$contract_claude")\",\"active_workstream\":\"$(json_escape "$workstream_file")\",\"change_type\":\"workstream-sync\",\"spawn_recommended\":false,\"contract_sync_required\":true,\"request_file\":\"$(json_escape "$request_file")\"}"
257
- printf '%s\n' "$event_json" >> "$event_file"
258
-
259
- if [[ -x "scripts/context-contract-sync.sh" ]]; then
260
- bash "scripts/context-contract-sync.sh" sync-event --json "$event_json"
261
- fi
262
-
263
- echo "[WorkstreamSync] Ensured $workstream_file for $capability_id."
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4
+ exec "$SCRIPT_DIR/lib/run-forge-helper.sh" "workstream-sync.sh" "$@"
@@ -1,4 +1,4 @@
1
- import type { GitHubStatus } from "../github/github";
1
+ import type { GitHubStatus } from "../github/contracts";
2
2
  import type { McpAgentRunnerName } from "../mcp/types";
3
3
  import type { ControllerAgent, ControllerTask } from "../controller/types";
4
4
 
@@ -63,13 +63,17 @@ export function installerNextSteps(result:Record<string,unknown>):string[]{
63
63
  .filter(Boolean)
64
64
  .slice(0,10);
65
65
  }
66
- export function registrationFrom(result:Record<string,unknown>,entry:RegistryEntry):ExternalPluginRegistrationInput{
66
+ export function registrationFrom(
67
+ result:Record<string,unknown>,
68
+ entry:RegistryEntry,
69
+ options: { packageIdentityVerified?: boolean } = {},
70
+ ):ExternalPluginRegistrationInput{
67
71
  if(result.registration){const input=result.registration as ExternalPluginRegistrationInput;if(input.pluginId!==entry.id)throw new Error('PLUGIN_INSTALLER_ID_MISMATCH');if(input.pluginVersion!==entry.version)throw new Error('PLUGIN_INSTALLER_VERSION_MISMATCH');return input;}
68
72
  const facts=result.providerInstall as Record<string,unknown>|undefined;
69
73
  if(!facts||facts.kind!=='desktop_operator'||entry.id!=='desktop_operator')throw new Error('PLUGIN_INSTALLER_REGISTRATION_MISSING');
70
74
  const expectedProviderVersion=entry.providerVersion??entry.version;
71
75
  const expectedProtocolVersion=entry.protocolVersion??'1.0';
72
- if(facts.pluginVersion!==expectedProviderVersion)throw new Error(`PLUGIN_INSTALLER_PROVIDER_VERSION_MISMATCH: expected ${expectedProviderVersion} for ${entry.id}@${entry.version}, actual ${String(facts.pluginVersion??'missing')}`);
76
+ if(facts.pluginVersion!==expectedProviderVersion&&!options.packageIdentityVerified)throw new Error(`PLUGIN_INSTALLER_PROVIDER_VERSION_MISMATCH: expected ${expectedProviderVersion} for ${entry.id}@${entry.version}, actual ${String(facts.pluginVersion??'missing')}`);
73
77
  if(facts.protocolVersion!==expectedProtocolVersion)throw new Error(`PLUGIN_INSTALLER_PROTOCOL_VERSION_MISMATCH: expected ${expectedProtocolVersion} for ${entry.id}@${entry.version}, actual ${String(facts.protocolVersion??'missing')}`);
74
78
  const socketPath=typeof facts.socketPath==='string'?facts.socketPath:'';const launchAgentLabel=typeof facts.launchAgentLabel==='string'?facts.launchAgentLabel:'';const expectedProgramContains=typeof facts.expectedProgramContains==='string'?facts.expectedProgramContains:'';
75
79
  if(!isAbsolute(socketPath)||!launchAgentLabel||!expectedProgramContains)throw new Error('PLUGIN_INSTALLER_PROVIDER_FACTS_INVALID');
@@ -86,7 +90,12 @@ function install(entry:RegistryEntry,controllerHome:string):Record<string,unknow
86
90
  const manifest=JSON.parse(readFileSync(manifestPath,'utf8')) as Record<string,unknown>;if(manifest.id!==entry.id||manifest.version!==entry.version)throw new Error(`PLUGIN_PACKAGE_IDENTITY_MISMATCH: ${entry.id}`);
87
91
  if(existsSync(finalRoot)){renameSync(finalRoot,backup);backed=true;}renameSync(stage,finalRoot);
88
92
  const result=installerJson(run(process.execPath,[within(finalRoot,entry.installer)],finalRoot,180000));
89
- const installed=installExternalPluginRegistration(controllerHome,registrationFrom(result,entry));
93
+ // The staged package manifest is checked against the catalog before the
94
+ // installer runs. Provider identity is then probed through the installed
95
+ // socket during registry sync. The installer's providerInstall version is
96
+ // useful diagnostics, but must not strand a verified package when that
97
+ // helper field is stale.
98
+ const installed=installExternalPluginRegistration(controllerHome,registrationFrom(result,entry,{packageIdentityVerified:true}));
90
99
  const repository=controllerPluginRepository(controllerHome);syncAssistantPluginRegistry(controllerHome,repository);const plugin=getAssistantPluginManifest(controllerHome,repository,entry.id);
91
100
  if(backed)rmSync(backup,{recursive:true,force:true});
92
101
  return{pluginId:entry.id,version:entry.version,packageRoot:finalRoot,registrationRevision:installed.revision,enabled:plugin.enabled,health:plugin.health,nextSteps:installerNextSteps(result)};
@@ -773,6 +773,8 @@ export function runControllerCheck(repoRoot: string, id: string, requestedTimeou
773
773
 
774
774
  export interface AsyncControllerCheckOptions {
775
775
  snapshot?: ControllerCheckSnapshot;
776
+ /** Explicit isolated authority for Candidate verification child processes. */
777
+ isolatedControllerHome?: string;
776
778
  requestedTimeoutMs?: number;
777
779
  onSpawn?: (pid: number) => void;
778
780
  subscriberId?: string;
@@ -912,12 +914,15 @@ async function executeControllerCheckAsync(
912
914
  check: ControllerCheck,
913
915
  timeoutMs: number,
914
916
  onSpawn?: (pid: number) => void,
917
+ isolatedControllerHome?: string,
915
918
  ): Promise<ControllerCheckResult> {
916
919
  const maxOutputBytes = 256 * 1024;
917
920
  const command = [check.command[0], ...check.command.slice(1)];
921
+ const childEnvironment = repositoryChildProcessEnvironment();
922
+ if (isolatedControllerHome?.trim()) childEnvironment.FORGE_CONTROLLER_HOME = resolve(isolatedControllerHome);
918
923
  const supervised = await runBoundedChild(check.command[0], check.command.slice(1), {
919
924
  cwd: resolve(repoRoot, check.cwd),
920
- env: repositoryChildProcessEnvironment(),
925
+ env: childEnvironment,
921
926
  timeoutMs,
922
927
  maxOutputBytes,
923
928
  stdio: 'capture',
@@ -1050,7 +1055,7 @@ export function runControllerCheckAsync(
1050
1055
  const result = await executeControllerCheckAsync(repoRoot, check, timeoutMs, (pid) => {
1051
1056
  lease?.setChildPid(pid);
1052
1057
  notifySpawn(pid);
1053
- });
1058
+ }, options.isolatedControllerHome);
1054
1059
  const completedRevision = currentControllerCheckRevision(repoRoot);
1055
1060
  const stale = completedRevision !== revision;
1056
1061
  const finalized = {
@@ -254,11 +254,17 @@ export function buildControllerContextPack(
254
254
  ? [...metadata.changedFiles.added, ...metadata.changedFiles.modified]
255
255
  : [];
256
256
  overlayChangedFiles = Array.from(new Set([...overlayChangedFiles, ...providerChangedFiles]));
257
+ const structuralEvidenceCurrent = structural.status === "ready"
258
+ && baselineRevisionMatches
259
+ && overlayChangedFiles.length === 0;
260
+ const structuralStatus = structural.status === "ready" && !structuralEvidenceCurrent
261
+ ? "stale"
262
+ : structural.status;
257
263
  structuralContext = {
258
264
  provider: "codegraph",
259
265
  requestedMode: structuralMode,
260
- status: indexSource === "repository_baseline" && (!baselineRevisionMatches || overlayChangedFiles.length > 0) && structural.status === "ready" ? "stale" : structural.status,
261
- requiredSatisfied: structuralMode !== "required" || (structural.status === "ready" && baselineRevisionMatches && overlayChangedFiles.length === 0),
266
+ status: structuralStatus,
267
+ requiredSatisfied: structuralMode !== "required" || structuralEvidenceCurrent,
262
268
  indexSource,
263
269
  overlayChangedFiles,
264
270
  baselineRevisionMatches,
@@ -285,24 +291,28 @@ export function buildControllerContextPack(
285
291
  const addStructuralPath = (path: string, reason: string, line?: number): void => {
286
292
  if (!path || !coveredByGlob(path, searchIncludeGlobs) || excludeGlobs.some((glob) => globMatches(glob, path))) return;
287
293
  const readable = readableFile(repoRoot, policy, path);
288
- if (!readable.ok) {
294
+ if (readable.ok === false) {
289
295
  deniedPaths.push({ path: readable.path, reason: readable.reason });
290
296
  return;
291
297
  }
292
298
  addReason(candidates, readable.path, reason, line);
293
299
  };
294
- for (const node of graph.nodes.slice(0, maxFiles * 6)) {
295
- addStructuralPath(node.filePath, `codegraph:context:${node.kind}:${node.name}`, node.startLine);
296
- }
297
- for (const node of graph.entryPoints) {
298
- addStructuralPath(node.filePath, `codegraph:entry:${node.name}`, node.startLine);
299
- }
300
- for (const path of graph.relatedFiles.slice(0, maxFiles * 6)) addStructuralPath(path, "codegraph:related");
301
- // Context search identifies the structural entry points. For a bounded
302
- // number of those files, enrich the same Context Plane call with direct
303
- // file dependencies/dependents. This is impact evidence, not a second
304
- // routing/tool surface, and remains completely off for ordinary Direct.
305
- if (structural.status === "ready") {
300
+ // Structural relationships may only influence discovery/materialization
301
+ // when the graph is current for this exact source identity. Stale graph
302
+ // nodes remain visible through structuralContext/impactContext hints, but
303
+ // must never steer which raw source GPT sees first.
304
+ if (structuralEvidenceCurrent) {
305
+ for (const node of graph.nodes.slice(0, maxFiles * 6)) {
306
+ addStructuralPath(node.filePath, `codegraph:context:${node.kind}:${node.name}`, node.startLine);
307
+ }
308
+ for (const node of graph.entryPoints) {
309
+ addStructuralPath(node.filePath, `codegraph:entry:${node.name}`, node.startLine);
310
+ }
311
+ for (const path of graph.relatedFiles.slice(0, maxFiles * 6)) addStructuralPath(path, "codegraph:related");
312
+ // Context search identifies the structural entry points. For a bounded
313
+ // number of those files, enrich the same Context Plane call with direct
314
+ // file dependencies/dependents. This is impact evidence, not a second
315
+ // routing/tool surface, and remains completely off for ordinary Direct.
306
316
  const primaryFiles = Array.from(new Set(graph.entryPoints.map((node) => node.filePath).filter(Boolean))).slice(0, 4);
307
317
  for (const filePath of primaryFiles) {
308
318
  const adjacency = queryStructural(structuralIndexRoot, { operation: "file_dependencies", filePath, limit: 40 });
@@ -323,10 +333,13 @@ export function buildControllerContextPack(
323
333
  }
324
334
  }
325
335
  }
326
- for (const path of metadata?.changedFiles.added ?? []) addStructuralPath(path, "codegraph:changed-file", 1);
327
- for (const path of metadata?.changedFiles.modified ?? []) addStructuralPath(path, "codegraph:changed-file", 1);
328
336
  for (const path of metadata?.changedFiles.removed ?? []) omitted.push({ path, reason: "codegraph reports file removed since index" });
329
- for (const path of overlayChangedFiles) addStructuralPath(path, "worktree:changed-file", 1);
337
+ // A repository-baseline graph deliberately runs against another checkout.
338
+ // Current worktree changes are still authoritative raw-source evidence,
339
+ // but they come from Git rather than stale CodeGraph metadata.
340
+ if (indexSource === "repository_baseline") {
341
+ for (const path of gitStatusChangedPaths(git.status)) addStructuralPath(path, "worktree:changed-file", 1);
342
+ }
330
343
  }
331
344
  }
332
345
 
@@ -430,7 +443,15 @@ export function buildControllerContextPack(
430
443
  return left.path.localeCompare(right.path);
431
444
  });
432
445
  const isExactKnownCandidate = (entry: { reasons: string[] }): boolean => entry.reasons.includes("explicit-known-path");
433
- const multiWaveEligible = retrievalMode !== "implementation";
446
+ // Adaptive closure is enabled whenever the caller asks for investigation/review
447
+ // evidence or leaves structural discovery enabled. A deliberately narrow
448
+ // implementation read with structural discovery disabled stays single-wave.
449
+ // This keeps the common exact-file edit path cheap without making the default
450
+ // implementation mode a permanent evidence-closure blind spot.
451
+ const multiWaveEligible = retrievalMode !== "implementation"
452
+ || structuralMode !== "off"
453
+ || impactDomains.length > 0
454
+ || explicitKnownPaths.length > 1;
434
455
  timingsMs.expansionBudgetMax = multiWaveEligible ? Math.min(6, Math.max(2, Math.ceil(maxFiles / 2))) : 0;
435
456
  const rankedExactKnownCount = rankedCandidates.filter(isExactKnownCandidate).length;
436
457
  const initialCandidateLimit = multiWaveEligible
@@ -511,7 +532,7 @@ export function buildControllerContextPack(
511
532
  }
512
533
  }
513
534
 
514
- // Investigation modes perform bounded internal evidence-closure waves after
535
+ // Evidence-closure eligible requests perform bounded internal waves after
515
536
  // Wave 1 raw source materialization. Each later wave derives concrete paths
516
537
  // from the source that was just read plus current CodeGraph adjacency, then
517
538
  // materializes those paths in the same context request.
@@ -794,6 +815,7 @@ export function buildControllerContextPack(
794
815
  ...expansionSignals,
795
816
  ...(likelyRelatedNotInspected.length > 0 ? ["likely_related_files_not_inspected"] : []),
796
817
  ])).slice(0, 80);
818
+ const materialRelationshipUnresolved = unresolvedExpansionPaths.length > 0;
797
819
  const readinessReasonCodes = Array.from(new Set([
798
820
  ...(files.length === 0 ? ["raw_source_unavailable"] : []),
799
821
  ...(rawSnippetTruncated ? ["raw_source_truncated"] : []),
@@ -804,9 +826,12 @@ export function buildControllerContextPack(
804
826
  ...(policyDeniedFiles > 0 || deniedPaths.length > 0 ? ["policy_denied_evidence"] : []),
805
827
  ...(skippedLargeFiles > 0 ? ["large_evidence_skipped"] : []),
806
828
  ...(expansionBudgetExhausted ? ["expansion_budget_exhausted"] : []),
829
+ ...(materialRelationshipUnresolved ? ["material_relationship_unresolved"] : []),
807
830
  ...(likelyRelatedNotInspected.length > 0 ? ["related_evidence_not_materialized"] : []),
808
831
  ])).slice(0, 40);
809
- const readinessStatus = files.length === 0 || (structuralMode === "required" && !structuralContext.requiredSatisfied)
832
+ const readinessStatus = files.length === 0
833
+ || (structuralMode === "required" && !structuralContext.requiredSatisfied)
834
+ || materialRelationshipUnresolved
810
835
  ? "insufficient" as const
811
836
  : readinessReasonCodes.length > 0
812
837
  ? "degraded" as const