@deftai/directive-core 0.106.0 → 0.108.0
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/dist/architecture/sor-preflight.js +2 -0
- package/dist/check/cached-orchestrator.js +28 -3
- package/dist/check/gate-lists.js +2 -0
- package/dist/check/named-cause.js +18 -3
- package/dist/codebase/provider.js +7 -1
- package/dist/delivery-attempt/handoff.js +1 -1
- package/dist/design-critique/citation-grammar.d.ts +53 -0
- package/dist/design-critique/citation-grammar.js +303 -0
- package/dist/design-critique/completed-arc-record.d.ts +62 -0
- package/dist/design-critique/completed-arc-record.js +232 -0
- package/dist/design-critique/exclusive-chip.d.ts +33 -0
- package/dist/design-critique/exclusive-chip.js +68 -0
- package/dist/design-critique/parent-audit.d.ts +59 -0
- package/dist/design-critique/parent-audit.js +121 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +35 -0
- package/dist/freshness/bind.d.ts +32 -3
- package/dist/freshness/bind.js +58 -12
- package/dist/hooks/classify/host-session-identity.d.ts +76 -0
- package/dist/hooks/classify/host-session-identity.js +470 -0
- package/dist/hooks/classify/index.d.ts +3 -2
- package/dist/hooks/classify/index.js +3 -2
- package/dist/hooks/classify/paths.d.ts +7 -0
- package/dist/hooks/classify/paths.js +35 -0
- package/dist/hooks/classify/stdin.d.ts +5 -0
- package/dist/hooks/classify/stdin.js +27 -0
- package/dist/hooks/dest-form.d.ts +46 -0
- package/dist/hooks/dest-form.js +575 -0
- package/dist/hooks/dispatcher.d.ts +69 -2
- package/dist/hooks/dispatcher.js +609 -45
- package/dist/hooks/fixtures/cases.d.ts +16 -2
- package/dist/hooks/fixtures/cases.js +175 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/scope.js +3 -1
- package/dist/init-deposit/agent-hooks.d.ts +2 -1
- package/dist/init-deposit/agent-hooks.js +8 -1
- package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
- package/dist/init-deposit/hook-runtime-travel.js +269 -0
- package/dist/intake/clause-derivation.js +9 -3
- package/dist/intake/github-auth-modes-cli.js +14 -2
- package/dist/intake/github-auth-modes.d.ts +54 -9
- package/dist/intake/github-auth-modes.js +326 -123
- package/dist/intake/issue-ingest.js +62 -2
- package/dist/intake/platform-capabilities.d.ts +9 -2
- package/dist/intake/platform-capabilities.js +38 -9
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
- package/dist/lifecycle/completed-write-guard.d.ts +48 -0
- package/dist/lifecycle/completed-write-guard.js +373 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/literal-acceptance/capture.d.ts +16 -0
- package/dist/literal-acceptance/capture.js +75 -7
- package/dist/literal-acceptance/evaluate.js +25 -3
- package/dist/literal-acceptance/index.d.ts +2 -2
- package/dist/literal-acceptance/index.js +2 -2
- package/dist/literal-acceptance/run.js +9 -3
- package/dist/literal-acceptance/types.d.ts +6 -0
- package/dist/literal-acceptance/types.js +6 -0
- package/dist/orphan-active/evaluate.d.ts +25 -0
- package/dist/orphan-active/evaluate.js +193 -128
- package/dist/orphan-active/index.d.ts +1 -0
- package/dist/orphan-active/index.js +1 -0
- package/dist/orphan-active/issue-state.d.ts +121 -0
- package/dist/orphan-active/issue-state.js +278 -0
- package/dist/platform/cursor-managed-runtime.d.ts +95 -0
- package/dist/platform/cursor-managed-runtime.js +241 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/platform/platform-capabilities.d.ts +23 -1
- package/dist/platform/platform-capabilities.js +55 -15
- package/dist/policy/ceremony-dial.js +5 -9
- package/dist/policy/host-hooks.js +4 -9
- package/dist/policy/org-force-on-migration.js +5 -10
- package/dist/policy/plan-extensions.d.ts +14 -2
- package/dist/policy/plan-extensions.js +24 -3
- package/dist/policy/product-signal.js +5 -10
- package/dist/policy/require-human-merge.js +5 -9
- package/dist/policy/resolve.js +77 -16
- package/dist/policy/runtime-authority.d.ts +18 -0
- package/dist/policy/runtime-authority.js +11 -0
- package/dist/policy/value-feedback.js +9 -18
- package/dist/policy/write-fence.js +3 -0
- package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
- package/dist/pr-closeout-attestable/evaluate.js +306 -0
- package/dist/pr-closeout-attestable/index.d.ts +2 -0
- package/dist/pr-closeout-attestable/index.js +2 -0
- package/dist/pr-protected-issues/gh.d.ts +6 -2
- package/dist/pr-protected-issues/gh.js +7 -3
- package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
- package/dist/pr-wait-mergeable/cascade.js +21 -1
- package/dist/pr-wait-mergeable/types.d.ts +2 -0
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
- package/dist/pr-wait-mergeable/wrappers.js +11 -0
- package/dist/preflight/evaluate.d.ts +15 -0
- package/dist/preflight/evaluate.js +29 -3
- package/dist/preflight-cache/evaluate.js +9 -3
- package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
- package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
- package/dist/product-first-done-gate/evaluate.js +53 -12
- package/dist/release/cli-drift-report.d.ts +68 -0
- package/dist/release/cli-drift-report.js +189 -0
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.js +1 -0
- package/dist/release/issue-state-fetch.d.ts +5 -3
- package/dist/release/issue-state-fetch.js +86 -21
- package/dist/release/native-steps.js +1 -0
- package/dist/release/pipeline.js +19 -0
- package/dist/release/types.d.ts +14 -0
- package/dist/render/constants.d.ts +4 -0
- package/dist/render/constants.js +11 -8
- package/dist/render/export-spec.js +31 -5
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/prd-render.d.ts +4 -1
- package/dist/render/prd-render.js +82 -23
- package/dist/render/project-render.js +14 -7
- package/dist/render/scope-outlook.d.ts +2 -0
- package/dist/render/scope-outlook.js +3 -0
- package/dist/render/spec-render.js +2 -2
- package/dist/resolution/package-manager.d.ts +33 -4
- package/dist/resolution/package-manager.js +210 -9
- package/dist/review-monitor/constants.d.ts +7 -0
- package/dist/review-monitor/constants.js +7 -0
- package/dist/review-monitor/github-lease.d.ts +98 -1
- package/dist/review-monitor/github-lease.js +182 -1
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +103 -2
- package/dist/scm/call.js +12 -3
- package/dist/scm/design-critique-chip.d.ts +38 -0
- package/dist/scm/design-critique-chip.js +187 -0
- package/dist/scm/gh-rest.d.ts +9 -1
- package/dist/scm/gh-rest.js +103 -0
- package/dist/scm/index.d.ts +1 -0
- package/dist/scm/index.js +1 -0
- package/dist/scm/main.d.ts +3 -0
- package/dist/scm/main.js +15 -1
- package/dist/scm/readiness-cli.d.ts +2 -0
- package/dist/scm/readiness-cli.js +42 -1
- package/dist/scm/readiness.d.ts +4 -1
- package/dist/scm/readiness.js +35 -5
- package/dist/scope/decompose.js +2 -1
- package/dist/scope/delivery-evidence.d.ts +27 -1
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/lifecycle-write.d.ts +24 -0
- package/dist/scope/lifecycle-write.js +64 -0
- package/dist/scope/main.js +3 -1
- package/dist/scope/project-definition-sync.js +14 -7
- package/dist/scope/transition.js +90 -11
- package/dist/scope-provenance/evaluate.js +5 -1
- package/dist/session/git.d.ts +42 -0
- package/dist/session/git.js +219 -3
- package/dist/session/occupancy.d.ts +239 -12
- package/dist/session/occupancy.js +820 -27
- package/dist/session/ritual-sentinel.d.ts +5 -0
- package/dist/session/ritual-sentinel.js +5 -0
- package/dist/session/session-ready.d.ts +5 -1
- package/dist/session/session-ready.js +98 -8
- package/dist/session/session-start.d.ts +17 -1
- package/dist/session/session-start.js +49 -17
- package/dist/session/toolchain-preflight.d.ts +13 -3
- package/dist/session/toolchain-preflight.js +93 -18
- package/dist/session/verify-session-ritual.d.ts +33 -3
- package/dist/session/verify-session-ritual.js +89 -19
- package/dist/slice/constants.d.ts +1 -1
- package/dist/spec-authority/constants.d.ts +10 -2
- package/dist/spec-authority/constants.js +53 -8
- package/dist/spec-authority/resolver.d.ts +3 -0
- package/dist/spec-authority/resolver.js +55 -7
- package/dist/subprocess/max-buffer.d.ts +14 -0
- package/dist/subprocess/max-buffer.js +15 -0
- package/dist/swarm/complete-cohort.d.ts +2 -0
- package/dist/swarm/complete-cohort.js +21 -13
- package/dist/swarm/index.d.ts +1 -0
- package/dist/swarm/index.js +1 -0
- package/dist/swarm/launch-cli.js +53 -33
- package/dist/swarm/launch.d.ts +77 -0
- package/dist/swarm/launch.js +248 -38
- package/dist/swarm/pre-dispatch-cli.js +2 -1
- package/dist/swarm/pre-dispatch.js +10 -1
- package/dist/swarm/subagent-status-dir.d.ts +27 -0
- package/dist/swarm/subagent-status-dir.js +42 -0
- package/dist/swarm/worktrees.d.ts +17 -3
- package/dist/swarm/worktrees.js +48 -4
- package/dist/triage/actions/index.d.ts +4 -0
- package/dist/triage/actions/index.js +6 -0
- package/dist/triage/evaluate/evaluate.d.ts +11 -0
- package/dist/triage/evaluate/evaluate.js +169 -0
- package/dist/triage/evaluate/github.d.ts +16 -0
- package/dist/triage/evaluate/github.js +153 -0
- package/dist/triage/evaluate/index.d.ts +9 -0
- package/dist/triage/evaluate/index.js +8 -0
- package/dist/triage/evaluate/paths.d.ts +7 -0
- package/dist/triage/evaluate/paths.js +25 -0
- package/dist/triage/evaluate/sink.d.ts +5 -0
- package/dist/triage/evaluate/sink.js +52 -0
- package/dist/triage/evaluate/types.d.ts +93 -0
- package/dist/triage/evaluate/types.js +13 -0
- package/dist/triage/evaluate/validity.d.ts +8 -0
- package/dist/triage/evaluate/validity.js +87 -0
- package/dist/triage/evaluate/value.d.ts +12 -0
- package/dist/triage/evaluate/value.js +33 -0
- package/dist/triage/evaluate/wip-census.d.ts +5 -0
- package/dist/triage/evaluate/wip-census.js +39 -0
- package/dist/triage/evaluate/worktrees.d.ts +8 -0
- package/dist/triage/evaluate/worktrees.js +68 -0
- package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
- package/dist/triage/evaluate/xbrief-refs.js +60 -0
- package/dist/triage/help/registry-data.d.ts +21 -7
- package/dist/triage/help/registry-data.js +53 -5
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- package/dist/triage/scope/mutations-core.d.ts +2 -6
- package/dist/triage/scope/mutations-core.js +23 -55
- package/dist/triage/scope-drift/add-ignore.js +5 -39
- package/dist/triage/subscribe/index.d.ts +2 -6
- package/dist/triage/subscribe/index.js +15 -65
- package/dist/triage/welcome/writers.js +15 -74
- package/dist/value/feedback-file.d.ts +17 -2
- package/dist/value/feedback-file.js +80 -6
- package/dist/vbrief-build/index.d.ts +2 -1
- package/dist/vbrief-build/index.js +2 -1
- package/dist/vbrief-build/parity-scenarios.js +5 -5
- package/dist/vbrief-build/project-definition-io.d.ts +63 -3
- package/dist/vbrief-build/project-definition-io.js +357 -31
- package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
- package/dist/vbrief-build/project-definition-mutation.js +43 -0
- package/dist/vbrief-reconcile/labels.d.ts +1 -0
- package/dist/vbrief-reconcile/labels.js +30 -0
- package/dist/vbrief-validate/conformance.d.ts +1 -0
- package/dist/vbrief-validate/conformance.js +85 -8
- package/dist/vbrief-validate/main.js +2 -0
- package/dist/vbrief-validate/precutover.js +5 -10
- package/dist/vbrief-validation/story-quality.d.ts +7 -0
- package/dist/vbrief-validation/story-quality.js +8 -1
- package/dist/verify-ac/clauses.d.ts +62 -2
- package/dist/verify-ac/clauses.js +183 -111
- package/dist/verify-ac/index.d.ts +1 -1
- package/dist/verify-ac/index.js +1 -1
- package/dist/verify-env/node-runtime.d.ts +8 -4
- package/dist/verify-env/node-runtime.js +9 -6
- package/dist/verify-env/toolchain-check.d.ts +35 -3
- package/dist/verify-env/toolchain-check.js +155 -36
- package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
- package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
- package/dist/xbrief-migrate/agents-header.js +69 -7
- package/package.json +7 -3
package/dist/swarm/launch.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { containedWrite } from "../fs/contained-write.js";
|
|
4
|
-
import { inferGithubAuthMode } from "../intake/github-auth-modes.js";
|
|
4
|
+
import { ENV_EXPECTED_GITHUB_LOGIN, FAILURE_INSTALLATION_IDENTITY_UNVERIFIABLE, FAILURE_MISSING_INJECTED_TOKEN, findInjectedToken, GITHUB_AUTH_MODE_HOST_GH, GITHUB_AUTH_MODE_INJECTED_TOKEN, inferGithubAuthMode, validateGithubAuthForWorker, } from "../intake/github-auth-modes.js";
|
|
5
5
|
import { getPlatformCapabilities } from "../intake/platform-capabilities.js";
|
|
6
6
|
import { hasArtifactSuffix, resolveLifecycleFolder, stripArtifactSuffix, } from "../layout/resolve.js";
|
|
7
7
|
import { evaluate as preflightEvaluate } from "../preflight/evaluate.js";
|
|
8
|
-
import { applyWorktreeOccupancy } from "../session/occupancy.js";
|
|
8
|
+
import { applyWorktreeOccupancy, releaseOccupancy } from "../session/occupancy.js";
|
|
9
9
|
import { issueNumbersFromPlan, scopeMetadataRank } from "../triage/queue/scope-walk.js";
|
|
10
10
|
import { selectionOrderingKey } from "../triage/queue/selection.js";
|
|
11
11
|
import { DEFAULT_BASE_BRANCH, EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK, GATE_ADVISE, GATE_ENFORCE, LEAF_CODING_WORKER_ROLE, } from "./constants.js";
|
|
@@ -25,6 +25,164 @@ export const defaultRuntimeAuthProbe = () => {
|
|
|
25
25
|
const report = getPlatformCapabilities();
|
|
26
26
|
return [report.runtimeMode, inferGithubAuthMode(report)];
|
|
27
27
|
};
|
|
28
|
+
const CREDENTIAL_VALUE_PATTERN = /^(ghp_|gho_|ghu_|ghs_|github_pat_)/i;
|
|
29
|
+
const INJECTION_MISSING_CREDENTIAL = "GH_TOKEN (or GITHUB_TOKEN / GH_ENTERPRISE_TOKEN)";
|
|
30
|
+
const INJECTION_DISPATCHER_REMEDY = "Dispatcher-side remedy: load an approved user-bearing worker credential into the dispatcher environment, then call prepareWorkerCredentialInjection before spawn. Do not fall back to the host gh token or continue under the maintainer identity. Installation credentials are not injectable; see #3693.";
|
|
31
|
+
const HELD_WORKER_TOKEN_VARS = [
|
|
32
|
+
"GH_TOKEN",
|
|
33
|
+
"GITHUB_TOKEN",
|
|
34
|
+
"GH_ENTERPRISE_TOKEN",
|
|
35
|
+
"GITHUB_ENTERPRISE_TOKEN",
|
|
36
|
+
];
|
|
37
|
+
function looksLikeCredentialValue(value) {
|
|
38
|
+
return CREDENTIAL_VALUE_PATTERN.test(value.trim());
|
|
39
|
+
}
|
|
40
|
+
function assertNoCredentialValue(value, label) {
|
|
41
|
+
if (looksLikeCredentialValue(value)) {
|
|
42
|
+
throw new Error(`${label} must not contain a credential value`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function sanitizeEnvelopeField(value, label) {
|
|
46
|
+
const cleaned = value.replace(/\r?\n/g, " ").trim();
|
|
47
|
+
assertNoCredentialValue(cleaned, label);
|
|
48
|
+
return cleaned;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Bind validation to the selected token. Stamp it into every gh token slot and
|
|
52
|
+
* drop GH_CONFIG_DIR so a host/enterprise config cannot authenticate as someone
|
|
53
|
+
* else. GH_HOST stays: it selects the API host, not the secret.
|
|
54
|
+
*/
|
|
55
|
+
function isolateHeldWorkerToken(environ, token) {
|
|
56
|
+
const isolated = { ...environ };
|
|
57
|
+
for (const name of HELD_WORKER_TOKEN_VARS) {
|
|
58
|
+
isolated[name] = token;
|
|
59
|
+
}
|
|
60
|
+
delete isolated.GH_CONFIG_DIR;
|
|
61
|
+
return isolated;
|
|
62
|
+
}
|
|
63
|
+
function boundWorkerSpawnEnv(token, expectedLogin) {
|
|
64
|
+
return {
|
|
65
|
+
GH_TOKEN: token,
|
|
66
|
+
GITHUB_TOKEN: token,
|
|
67
|
+
GH_ENTERPRISE_TOKEN: token,
|
|
68
|
+
[ENV_EXPECTED_GITHUB_LOGIN]: expectedLogin,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Dispatch-envelope half of the runtime / GitHub auth-mode contract (#1557 / #1351).
|
|
73
|
+
* Policy labels only — never a token value.
|
|
74
|
+
*/
|
|
75
|
+
export function formatDispatchAuthEnvelope(fields) {
|
|
76
|
+
const runtimeMode = sanitizeEnvelopeField(fields.runtimeMode, "runtime_mode");
|
|
77
|
+
const githubAuthMode = sanitizeEnvelopeField(fields.githubAuthMode, "github_auth_mode");
|
|
78
|
+
const lines = [
|
|
79
|
+
"## Runtime and GitHub auth mode",
|
|
80
|
+
"",
|
|
81
|
+
`- runtime_mode: ${runtimeMode}`,
|
|
82
|
+
`- github_auth_mode: ${githubAuthMode}`,
|
|
83
|
+
];
|
|
84
|
+
if (fields.expectedGithubLogin !== undefined && fields.expectedGithubLogin !== null) {
|
|
85
|
+
const login = sanitizeEnvelopeField(fields.expectedGithubLogin, "expected_github_login");
|
|
86
|
+
if (login.length > 0) {
|
|
87
|
+
lines.push(`- expected_github_login: ${login}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return `${lines.join("\n")}\n`;
|
|
91
|
+
}
|
|
92
|
+
function blockedInjection(fields) {
|
|
93
|
+
const detail = fields.detail.startsWith("BLOCKED") ? fields.detail : `BLOCKED: ${fields.detail}`;
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
blocked: true,
|
|
97
|
+
githubAuthMode: fields.githubAuthMode,
|
|
98
|
+
runtimeMode: fields.runtimeMode,
|
|
99
|
+
missingCredential: fields.missingCredential ?? INJECTION_MISSING_CREDENTIAL,
|
|
100
|
+
remedy: INJECTION_DISPATCHER_REMEDY,
|
|
101
|
+
detail,
|
|
102
|
+
failureKind: fields.failureKind,
|
|
103
|
+
envelopeSection: formatDispatchAuthEnvelope({
|
|
104
|
+
runtimeMode: fields.runtimeMode ?? "unknown",
|
|
105
|
+
githubAuthMode: fields.githubAuthMode,
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Validate a held user-bearing worker credential and prepare spawn-time injection (#1351).
|
|
111
|
+
*
|
|
112
|
+
* Comparison is opt-in on the validator. This path always stamps
|
|
113
|
+
* `DEFT_EXPECTED_GITHUB_LOGIN` after a successful user-principal check so the
|
|
114
|
+
* worker envelope does not inherit "any user token is approved."
|
|
115
|
+
*
|
|
116
|
+
* Installation credentials fail closed via the existing validator (#3693).
|
|
117
|
+
* Token values belong only in the returned `spawnEnv` — never in prompts,
|
|
118
|
+
* transcripts, or manifest entries.
|
|
119
|
+
*/
|
|
120
|
+
export function prepareWorkerCredentialInjection(request) {
|
|
121
|
+
const environ = request.environ ?? process.env;
|
|
122
|
+
const runtimeMode = request.runtimeMode ?? null;
|
|
123
|
+
const token = findInjectedToken(environ);
|
|
124
|
+
const grokBuild = request.dispatchPath === "grok-build";
|
|
125
|
+
const effectiveMode = grokBuild || token !== null ? GITHUB_AUTH_MODE_INJECTED_TOKEN : request.githubAuthMode;
|
|
126
|
+
if (effectiveMode === GITHUB_AUTH_MODE_HOST_GH && token === null) {
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
injected: false,
|
|
130
|
+
githubAuthMode: GITHUB_AUTH_MODE_HOST_GH,
|
|
131
|
+
runtimeMode,
|
|
132
|
+
expectedLogin: null,
|
|
133
|
+
spawnEnv: {},
|
|
134
|
+
envelopeSection: formatDispatchAuthEnvelope({
|
|
135
|
+
runtimeMode: runtimeMode ?? "local-unsandboxed",
|
|
136
|
+
githubAuthMode: GITHUB_AUTH_MODE_HOST_GH,
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (token === null) {
|
|
141
|
+
return blockedInjection({
|
|
142
|
+
githubAuthMode: GITHUB_AUTH_MODE_INJECTED_TOKEN,
|
|
143
|
+
runtimeMode,
|
|
144
|
+
failureKind: FAILURE_MISSING_INJECTED_TOKEN,
|
|
145
|
+
missingCredential: INJECTION_MISSING_CREDENTIAL,
|
|
146
|
+
detail: "missing worker credential GH_TOKEN (or GITHUB_TOKEN / GH_ENTERPRISE_TOKEN) for a write-requiring injected-token dispatch",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const isolatedEnviron = isolateHeldWorkerToken(environ, token);
|
|
150
|
+
const validated = validateGithubAuthForWorker(GITHUB_AUTH_MODE_INJECTED_TOKEN, {
|
|
151
|
+
environ: isolatedEnviron,
|
|
152
|
+
runtimeReport: { runtimeMode: runtimeMode ?? "cloud-headless" },
|
|
153
|
+
repo: request.repo,
|
|
154
|
+
runGh: request.runGh,
|
|
155
|
+
expectedPrincipal: request.expectedPrincipal,
|
|
156
|
+
cwd: request.cwd,
|
|
157
|
+
});
|
|
158
|
+
if (!validated.ok || validated.login === null || validated.login.trim().length === 0) {
|
|
159
|
+
const installation = validated.failureKind === FAILURE_INSTALLATION_IDENTITY_UNVERIFIABLE;
|
|
160
|
+
return blockedInjection({
|
|
161
|
+
githubAuthMode: GITHUB_AUTH_MODE_INJECTED_TOKEN,
|
|
162
|
+
runtimeMode,
|
|
163
|
+
failureKind: validated.failureKind,
|
|
164
|
+
detail: installation
|
|
165
|
+
? `${validated.detail} Installation credentials are not injectable (#3693).`
|
|
166
|
+
: validated.detail,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const expectedLogin = validated.login.trim();
|
|
170
|
+
assertNoCredentialValue(expectedLogin, "expected_github_login");
|
|
171
|
+
const spawnEnv = boundWorkerSpawnEnv(token, expectedLogin);
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
injected: true,
|
|
175
|
+
githubAuthMode: GITHUB_AUTH_MODE_INJECTED_TOKEN,
|
|
176
|
+
runtimeMode,
|
|
177
|
+
expectedLogin,
|
|
178
|
+
spawnEnv,
|
|
179
|
+
envelopeSection: formatDispatchAuthEnvelope({
|
|
180
|
+
runtimeMode: runtimeMode ?? "cloud-headless",
|
|
181
|
+
githubAuthMode: GITHUB_AUTH_MODE_INJECTED_TOKEN,
|
|
182
|
+
expectedGithubLogin: expectedLogin,
|
|
183
|
+
}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
28
186
|
function loadJson(path) {
|
|
29
187
|
try {
|
|
30
188
|
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -498,6 +656,10 @@ export function buildManifest(resolved, options) {
|
|
|
498
656
|
if (options.githubAuthMode !== undefined && options.githubAuthMode !== null) {
|
|
499
657
|
entry.github_auth_mode = options.githubAuthMode;
|
|
500
658
|
}
|
|
659
|
+
if (options.expectedGithubLogin !== undefined && options.expectedGithubLogin !== null) {
|
|
660
|
+
assertNoCredentialValue(options.expectedGithubLogin, "expected_github_login");
|
|
661
|
+
entry.expected_github_login = options.expectedGithubLogin;
|
|
662
|
+
}
|
|
501
663
|
manifest.push(entry);
|
|
502
664
|
}
|
|
503
665
|
return manifest;
|
|
@@ -533,7 +695,23 @@ function splitCsv(values) {
|
|
|
533
695
|
return out;
|
|
534
696
|
}
|
|
535
697
|
export function swarmLaunch(args) {
|
|
698
|
+
if (args.parseError !== undefined && args.parseError !== null) {
|
|
699
|
+
return {
|
|
700
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
701
|
+
stdout: "",
|
|
702
|
+
stderr: `Error: ${args.parseError}\n`,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
536
705
|
const projectRoot = resolve(args.projectRoot ?? process.cwd());
|
|
706
|
+
if (args.sessionId !== undefined &&
|
|
707
|
+
args.sessionId !== null &&
|
|
708
|
+
(args.sessionId.trim().length === 0 || args.sessionId.trim().startsWith("--"))) {
|
|
709
|
+
return {
|
|
710
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
711
|
+
stdout: "",
|
|
712
|
+
stderr: "Error: --session-id requires a non-empty owner identity.\n",
|
|
713
|
+
};
|
|
714
|
+
}
|
|
537
715
|
const tokens = [...splitCsv(args.stories ?? []), ...splitCsv(args.paths ?? [])];
|
|
538
716
|
if (tokens.length === 0) {
|
|
539
717
|
return {
|
|
@@ -619,17 +797,52 @@ export function swarmLaunch(args) {
|
|
|
619
797
|
}
|
|
620
798
|
const operatorApproval = args.operatorApproval ??
|
|
621
799
|
`task swarm:launch (${args.autonomous ? "autonomous" : "interactive"})`;
|
|
800
|
+
const occupancy = applyWorktreeOccupancy(projectRoot, {
|
|
801
|
+
sessionId: args.sessionId ?? undefined,
|
|
802
|
+
env: args.environ ?? process.env,
|
|
803
|
+
intent: "swarm",
|
|
804
|
+
});
|
|
805
|
+
if (occupancy.code !== 0) {
|
|
806
|
+
return {
|
|
807
|
+
exitCode: EXIT_GATE_FAILED,
|
|
808
|
+
stdout: "",
|
|
809
|
+
stderr: `${occupancy.message}\n`,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
// Heartbeat on an existing owner is not a new claim -- only release a lease
|
|
813
|
+
// this process just minted (#3649 paired failure clause).
|
|
814
|
+
const newlyClaimed = occupancy.action === "claimed";
|
|
815
|
+
const failAfterClaim = (exitCode, stderr) => {
|
|
816
|
+
if (newlyClaimed) {
|
|
817
|
+
const recovery = "Occupancy release failed after this launch error; the lease may still be live. " +
|
|
818
|
+
"The occupant may release (occupancy:release / session:end). A replacement owner must " +
|
|
819
|
+
"run session:start --steal --confirm --occupant <reported-session-id> " +
|
|
820
|
+
"--session-id=<your-session-id> to align lease and ritual state.";
|
|
821
|
+
try {
|
|
822
|
+
const release = args.releaseOccupancyFn ?? releaseOccupancy;
|
|
823
|
+
const decision = release(projectRoot, {
|
|
824
|
+
sessionId: occupancy.sessionId,
|
|
825
|
+
env: args.environ ?? process.env,
|
|
826
|
+
});
|
|
827
|
+
if (decision.code !== 0) {
|
|
828
|
+
stderr = `${stderr}\n${recovery} ${decision.message}\n`;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
catch (exc) {
|
|
832
|
+
// Do not replace the original structured launch error; tell the operator
|
|
833
|
+
// the lease may still deny later sessions (#3649 Greptile P1).
|
|
834
|
+
stderr = `${stderr}\n${recovery} ${String(exc)}\n`;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return { exitCode, stdout: "", stderr };
|
|
838
|
+
};
|
|
622
839
|
let worktreeRecordMap = new Map();
|
|
623
840
|
if (args.worktreeMap !== undefined && args.worktreeMap !== null) {
|
|
624
841
|
const resolver = args.worktreeResolver ?? resolveWorktreeMap;
|
|
625
842
|
try {
|
|
626
843
|
const payload = JSON.parse(readFileSync(args.worktreeMap, "utf8"));
|
|
627
844
|
if (!Array.isArray(payload)) {
|
|
628
|
-
return {
|
|
629
|
-
exitCode: EXIT_CONFIG_ERROR,
|
|
630
|
-
stdout: "",
|
|
631
|
-
stderr: `Error: --worktree-map ${args.worktreeMap} must contain a JSON array of records.\n`,
|
|
632
|
-
};
|
|
845
|
+
return failAfterClaim(EXIT_CONFIG_ERROR, `Error: --worktree-map ${args.worktreeMap} must contain a JSON array of records.\n`);
|
|
633
846
|
}
|
|
634
847
|
const records = resolver(payload, args.baseBranch ?? DEFAULT_BASE_BRANCH, !(args.noCreateWorktrees ?? false), {
|
|
635
848
|
repoRoot: projectRoot,
|
|
@@ -637,11 +850,7 @@ export function swarmLaunch(args) {
|
|
|
637
850
|
worktreeRecordMap = new Map(records.map((r) => [r.story_id, r]));
|
|
638
851
|
}
|
|
639
852
|
catch (exc) {
|
|
640
|
-
return {
|
|
641
|
-
exitCode: EXIT_CONFIG_ERROR,
|
|
642
|
-
stdout: "",
|
|
643
|
-
stderr: `Error: worktree map resolution failed: ${String(exc)}\n`,
|
|
644
|
-
};
|
|
853
|
+
return failAfterClaim(EXIT_CONFIG_ERROR, `Error: worktree map resolution failed: ${String(exc)}\n`);
|
|
645
854
|
}
|
|
646
855
|
}
|
|
647
856
|
let runtimeMode;
|
|
@@ -651,7 +860,7 @@ export function swarmLaunch(args) {
|
|
|
651
860
|
[runtimeMode, githubAuthMode] = probe();
|
|
652
861
|
}
|
|
653
862
|
catch (exc) {
|
|
654
|
-
return
|
|
863
|
+
return failAfterClaim(EXIT_CONFIG_ERROR, `Error: ${String(exc)}\n`);
|
|
655
864
|
}
|
|
656
865
|
let resolvedModel = null;
|
|
657
866
|
let modelSource = null;
|
|
@@ -664,11 +873,7 @@ export function swarmLaunch(args) {
|
|
|
664
873
|
// would emit an exit-0 manifest with no model and no error to follow. Match
|
|
665
874
|
// verify:routing, which treats the same state as a config error (#1739).
|
|
666
875
|
if (route.source === "invalid") {
|
|
667
|
-
return {
|
|
668
|
-
exitCode: EXIT_CONFIG_ERROR,
|
|
669
|
-
stdout: "",
|
|
670
|
-
stderr: `Error: routing gate misconfigured: ${route.error ?? "invalid routing decision"}\n`,
|
|
671
|
-
};
|
|
876
|
+
return failAfterClaim(EXIT_CONFIG_ERROR, `Error: routing gate misconfigured: ${route.error ?? "invalid routing decision"}\n`);
|
|
672
877
|
}
|
|
673
878
|
if (route.decided) {
|
|
674
879
|
resolvedModel = route.model;
|
|
@@ -681,16 +886,28 @@ export function swarmLaunch(args) {
|
|
|
681
886
|
? dispatchProviderFor(backend.backend_id)
|
|
682
887
|
: null;
|
|
683
888
|
const workerRoleValue = routingFile !== null || backend !== null ? LEAF_CODING_WORKER_ROLE : null;
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
889
|
+
const launchEnviron = args.environ ?? process.env;
|
|
890
|
+
let expectedGithubLogin = null;
|
|
891
|
+
// Only the injected-token launch path auto-validates (and fail-closes on a
|
|
892
|
+
// missing token). An ambient GH_TOKEN on a host-gh probe is often the
|
|
893
|
+
// maintainer workaround, not a worker credential; local-hybrid injection is
|
|
894
|
+
// the explicit prepareWorkerCredentialInjection call.
|
|
895
|
+
if (githubAuthMode === GITHUB_AUTH_MODE_INJECTED_TOKEN) {
|
|
896
|
+
const injection = prepareWorkerCredentialInjection({
|
|
897
|
+
environ: launchEnviron,
|
|
898
|
+
githubAuthMode,
|
|
899
|
+
runtimeMode,
|
|
900
|
+
dispatchPath: "grok-build",
|
|
901
|
+
runGh: args.runGh,
|
|
902
|
+
expectedPrincipal: args.expectedPrincipal,
|
|
903
|
+
cwd: projectRoot,
|
|
904
|
+
});
|
|
905
|
+
if (!injection.ok) {
|
|
906
|
+
return failAfterClaim(EXIT_GATE_FAILED, `${injection.detail}\n${injection.remedy}\n`);
|
|
907
|
+
}
|
|
908
|
+
if (injection.injected) {
|
|
909
|
+
expectedGithubLogin = injection.expectedLogin;
|
|
910
|
+
}
|
|
694
911
|
}
|
|
695
912
|
const manifest = buildManifest(ordered, {
|
|
696
913
|
projectRoot,
|
|
@@ -708,6 +925,7 @@ export function swarmLaunch(args) {
|
|
|
708
925
|
modelSource,
|
|
709
926
|
runtimeMode,
|
|
710
927
|
githubAuthMode,
|
|
928
|
+
expectedGithubLogin,
|
|
711
929
|
occupancySessionId: occupancy.sessionId,
|
|
712
930
|
});
|
|
713
931
|
const rendered = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
@@ -729,11 +947,7 @@ export function swarmLaunch(args) {
|
|
|
729
947
|
});
|
|
730
948
|
}
|
|
731
949
|
catch (exc) {
|
|
732
|
-
return {
|
|
733
|
-
exitCode: EXIT_CONFIG_ERROR,
|
|
734
|
-
stdout: "",
|
|
735
|
-
stderr: `Error: could not persist launch occupancy_session_id: ${String(exc)}\n`,
|
|
736
|
-
};
|
|
950
|
+
return failAfterClaim(EXIT_CONFIG_ERROR, `Error: could not persist launch occupancy_session_id: ${String(exc)}\n`);
|
|
737
951
|
}
|
|
738
952
|
if (args.output !== undefined && args.output !== null) {
|
|
739
953
|
try {
|
|
@@ -749,11 +963,7 @@ export function swarmLaunch(args) {
|
|
|
749
963
|
});
|
|
750
964
|
}
|
|
751
965
|
catch (exc) {
|
|
752
|
-
return {
|
|
753
|
-
exitCode: EXIT_CONFIG_ERROR,
|
|
754
|
-
stdout: "",
|
|
755
|
-
stderr: `Error: could not write --output ${args.output}: ${String(exc)}\n`,
|
|
756
|
-
};
|
|
966
|
+
return failAfterClaim(EXIT_CONFIG_ERROR, `Error: could not write --output ${args.output}: ${String(exc)}\n`);
|
|
757
967
|
}
|
|
758
968
|
}
|
|
759
969
|
void args.noAudit;
|
|
@@ -85,7 +85,8 @@ Before spawning a peer implement leaf: run with default --action begin.
|
|
|
85
85
|
exit 1 active deny / gate block (do not spawn)
|
|
86
86
|
exit 2 config / usage error
|
|
87
87
|
|
|
88
|
-
Takeover: --action cancel, then pre-dispatch begin again
|
|
88
|
+
Takeover after REDISPATCH_OK: --action cancel, then pre-dispatch begin again
|
|
89
|
+
(not concurrent dual active; a killed worker stays running until cancelled).
|
|
89
90
|
Terminal: --action complete [--status succeeded|failed|cancelled|blocked]
|
|
90
91
|
|
|
91
92
|
Options:
|
|
@@ -12,6 +12,7 @@ import { existsSync, realpathSync } from "node:fs";
|
|
|
12
12
|
import { isAbsolute, normalize, resolve } from "node:path";
|
|
13
13
|
import { activeAttempts, beginAttempt, completeAttemptOnDisk, evaluatePreDispatch, hasActiveAttempt, listUnitLedgers, loadOrCreateUnitLedger, loadUnitLedger, markBlocked, saveUnitLedger, withUnitLock, } from "../delivery-attempt/index.js";
|
|
14
14
|
import { EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK } from "./constants.js";
|
|
15
|
+
import { ensureSubagentStatusDir, looksLikeWorktreeDir } from "./subagent-status-dir.js";
|
|
15
16
|
import { runText } from "./subprocess.js";
|
|
16
17
|
/** Default workflow id for drive-to:merge-ready implement leaves. */
|
|
17
18
|
export const IMPLEMENT_LEAF_WORKFLOW_ID = "drive-to:merge-ready";
|
|
@@ -170,6 +171,14 @@ function runBegin(input) {
|
|
|
170
171
|
const { scopeId, targetId, workflowId } = unitFields(input);
|
|
171
172
|
const sourceRevision = resolveSourceRevision(input.projectRoot, input.sourceRevision);
|
|
172
173
|
const trigger = input.trigger ?? "automatic";
|
|
174
|
+
// Arm heartbeat scratch so verify:subagent-alive can return REDISPATCH_OK
|
|
175
|
+
// instead of exit 2 ("scratch dir does not exist") after a host-kill (#3730).
|
|
176
|
+
// A bare relative worktree name (`b3730`) is opaque to the branch-vs-path
|
|
177
|
+
// heuristic, so accept an on-disk worktree as the second arming signal.
|
|
178
|
+
const armTarget = resolve(input.projectRoot, input.targetId.trim());
|
|
179
|
+
if (looksLikeFilesystemTarget(input.targetId) || looksLikeWorktreeDir(armTarget)) {
|
|
180
|
+
ensureSubagentStatusDir(armTarget);
|
|
181
|
+
}
|
|
173
182
|
// Exclusive lock → reload → evaluate → begin+save (same decision under lock;
|
|
174
183
|
// no stale preview decision on the success path).
|
|
175
184
|
try {
|
|
@@ -366,7 +375,7 @@ export function formatPreDispatchReport(result) {
|
|
|
366
375
|
lines.push(` attempt: ${result.attempt.attemptId} status=${result.attempt.status}`);
|
|
367
376
|
}
|
|
368
377
|
if (result.exitCode === EXIT_GATE_FAILED && result.decision === "DENY_DUPLICATE_ACTIVE") {
|
|
369
|
-
lines.push(" hint: do not spawn;
|
|
378
|
+
lines.push(" hint: do not spawn; a killed worker stays running until cancelled.", " Takeover: task swarm:pre-dispatch -- --scope-id <id> --target-id <target> --action cancel", " then the same command without --action (begin). REDISPATCH_OK does not lift DENY_DUPLICATE_ACTIVE.");
|
|
370
379
|
}
|
|
371
380
|
return lines.join("\n");
|
|
372
381
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arm the shipped sub-agent heartbeat path (#3730 / #1365 / #2824).
|
|
3
|
+
*
|
|
4
|
+
* `verify:subagent-alive` exits 2 when the scratch directory is missing, which
|
|
5
|
+
* looks like "never started" instead of REDISPATCH_OK. Parents mkdir at
|
|
6
|
+
* worktree create and at pre-dispatch begin so a later missing record is
|
|
7
|
+
* exit 1. Does not write heartbeat records or liveness onto the C2 manifest.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* True when a resolved path is an on-disk git worktree, i.e. it carries a `.git`
|
|
11
|
+
* entry (a file for `git worktree add` trees, a directory for the main clone).
|
|
12
|
+
*
|
|
13
|
+
* Arming cannot key off `looksLikeFilesystemTarget` alone: that predicate exists
|
|
14
|
+
* to keep branch refs opaque and so answers false for a bare relative worktree
|
|
15
|
+
* name like `b3730`, leaving the liveness gate at its exit-2 config error for a
|
|
16
|
+
* supported dispatch form. Existence alone is too loose in the other direction —
|
|
17
|
+
* a branch named `docs` resolves onto the real `docs/` tree and would mkdir a
|
|
18
|
+
* stray scratch dir in the source checkout. The `.git` marker separates the two.
|
|
19
|
+
*/
|
|
20
|
+
export declare function looksLikeWorktreeDir(resolvedPath: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Create `<worktree>/.deft-scratch/subagent-status/` when the worktree exists.
|
|
23
|
+
* Returns the directory path, or null when the worktree is not on disk yet
|
|
24
|
+
* (do not mkdir a stray tree for a branch-name target).
|
|
25
|
+
*/
|
|
26
|
+
export declare function ensureSubagentStatusDir(worktreeRoot: string): string | null;
|
|
27
|
+
//# sourceMappingURL=subagent-status-dir.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arm the shipped sub-agent heartbeat path (#3730 / #1365 / #2824).
|
|
3
|
+
*
|
|
4
|
+
* `verify:subagent-alive` exits 2 when the scratch directory is missing, which
|
|
5
|
+
* looks like "never started" instead of REDISPATCH_OK. Parents mkdir at
|
|
6
|
+
* worktree create and at pre-dispatch begin so a later missing record is
|
|
7
|
+
* exit 1. Does not write heartbeat records or liveness onto the C2 manifest.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { defaultScratchDir } from "../orchestration/subagent-monitor.js";
|
|
12
|
+
/**
|
|
13
|
+
* True when a resolved path is an on-disk git worktree, i.e. it carries a `.git`
|
|
14
|
+
* entry (a file for `git worktree add` trees, a directory for the main clone).
|
|
15
|
+
*
|
|
16
|
+
* Arming cannot key off `looksLikeFilesystemTarget` alone: that predicate exists
|
|
17
|
+
* to keep branch refs opaque and so answers false for a bare relative worktree
|
|
18
|
+
* name like `b3730`, leaving the liveness gate at its exit-2 config error for a
|
|
19
|
+
* supported dispatch form. Existence alone is too loose in the other direction —
|
|
20
|
+
* a branch named `docs` resolves onto the real `docs/` tree and would mkdir a
|
|
21
|
+
* stray scratch dir in the source checkout. The `.git` marker separates the two.
|
|
22
|
+
*/
|
|
23
|
+
export function looksLikeWorktreeDir(resolvedPath) {
|
|
24
|
+
const trimmed = resolvedPath.trim();
|
|
25
|
+
if (trimmed.length === 0)
|
|
26
|
+
return false;
|
|
27
|
+
return existsSync(join(trimmed, ".git"));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create `<worktree>/.deft-scratch/subagent-status/` when the worktree exists.
|
|
31
|
+
* Returns the directory path, or null when the worktree is not on disk yet
|
|
32
|
+
* (do not mkdir a stray tree for a branch-name target).
|
|
33
|
+
*/
|
|
34
|
+
export function ensureSubagentStatusDir(worktreeRoot) {
|
|
35
|
+
const trimmed = worktreeRoot.trim();
|
|
36
|
+
if (trimmed.length === 0 || !existsSync(trimmed))
|
|
37
|
+
return null;
|
|
38
|
+
const dir = defaultScratchDir(trimmed);
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=subagent-status-dir.js.map
|
|
@@ -21,6 +21,9 @@ export declare class WorktreeMapConfigError extends Error {
|
|
|
21
21
|
export declare class WorktreePathEscapeError extends WorktreeMapError {
|
|
22
22
|
name: string;
|
|
23
23
|
}
|
|
24
|
+
export declare class WorktreeRevisionMismatchError extends WorktreeMapError {
|
|
25
|
+
name: string;
|
|
26
|
+
}
|
|
24
27
|
export type GitRunner = (args: readonly string[], cwd: string) => TextCaptureResult;
|
|
25
28
|
export declare const defaultGitRunner: GitRunner;
|
|
26
29
|
/** Case-normalized comparison key for worktree-path equality. */
|
|
@@ -30,9 +33,20 @@ export interface WorktreeRecord {
|
|
|
30
33
|
readonly worktree_path: string;
|
|
31
34
|
readonly base_branch: string;
|
|
32
35
|
}
|
|
33
|
-
/**
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
+
/** One `git worktree list --porcelain` record. `head` is the commit OID or null. */
|
|
37
|
+
export interface WorktreePorcelainEntry {
|
|
38
|
+
readonly branch: string | null;
|
|
39
|
+
readonly head: string | null;
|
|
40
|
+
}
|
|
41
|
+
/** Parse `git worktree list --porcelain` into `{compareKey: {branch, head}}`. */
|
|
42
|
+
export declare function parseWorktreePorcelain(text: string): Map<string, WorktreePorcelainEntry>;
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a story-to-worktree mapping into normalized C3 records (frozen contract).
|
|
45
|
+
*
|
|
46
|
+
* Registered paths are reused only when their porcelain HEAD OID matches the
|
|
47
|
+
* requested base OID. That comparison is a snapshot at resolution time — not a
|
|
48
|
+
* pin on the worker's start revision (`swarm:launch` emits a manifest and stops).
|
|
49
|
+
*/
|
|
36
50
|
export declare function resolveWorktreeMap(mapping: readonly Record<string, unknown>[], baseBranch: string, createMissing?: boolean, options?: {
|
|
37
51
|
repoRoot?: string;
|
|
38
52
|
git?: GitRunner;
|
package/dist/swarm/worktrees.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { isAbsolute, resolve as pathResolve } from "node:path";
|
|
3
3
|
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
4
4
|
import { C3_FIELDS } from "./constants.js";
|
|
5
|
+
import { ensureSubagentStatusDir } from "./subagent-status-dir.js";
|
|
5
6
|
import { runText } from "./subprocess.js";
|
|
6
7
|
export class WorktreeMapError extends Error {
|
|
7
8
|
name = "WorktreeMapError";
|
|
@@ -24,6 +25,9 @@ export class WorktreeMapConfigError extends Error {
|
|
|
24
25
|
export class WorktreePathEscapeError extends WorktreeMapError {
|
|
25
26
|
name = "WorktreePathEscapeError";
|
|
26
27
|
}
|
|
28
|
+
export class WorktreeRevisionMismatchError extends WorktreeMapError {
|
|
29
|
+
name = "WorktreeRevisionMismatchError";
|
|
30
|
+
}
|
|
27
31
|
export const defaultGitRunner = (args, cwd) => runText(["git", ...args], { cwd });
|
|
28
32
|
function resolvePath(raw, repoRoot) {
|
|
29
33
|
const candidate = isAbsolute(raw) ? raw : pathResolve(repoRoot, raw);
|
|
@@ -44,14 +48,15 @@ function assertWorktreePathContained(repoRoot, worktreePath) {
|
|
|
44
48
|
export function compareKey(pathStr) {
|
|
45
49
|
return pathStr.replace(/\\/g, "/").toLowerCase();
|
|
46
50
|
}
|
|
47
|
-
/** Parse `git worktree list --porcelain` into `{compareKey: branch
|
|
51
|
+
/** Parse `git worktree list --porcelain` into `{compareKey: {branch, head}}`. */
|
|
48
52
|
export function parseWorktreePorcelain(text) {
|
|
49
53
|
const registered = new Map();
|
|
50
54
|
let currentPath = null;
|
|
51
55
|
let currentBranch = null;
|
|
56
|
+
let currentHead = null;
|
|
52
57
|
const flush = () => {
|
|
53
58
|
if (currentPath !== null) {
|
|
54
|
-
registered.set(compareKey(currentPath), currentBranch);
|
|
59
|
+
registered.set(compareKey(currentPath), { branch: currentBranch, head: currentHead });
|
|
55
60
|
}
|
|
56
61
|
};
|
|
57
62
|
for (const line of text.split("\n")) {
|
|
@@ -59,6 +64,11 @@ export function parseWorktreePorcelain(text) {
|
|
|
59
64
|
flush();
|
|
60
65
|
currentPath = pathResolve(line.slice("worktree ".length).trim());
|
|
61
66
|
currentBranch = null;
|
|
67
|
+
currentHead = null;
|
|
68
|
+
}
|
|
69
|
+
else if (line.startsWith("HEAD ")) {
|
|
70
|
+
const oid = line.slice("HEAD ".length).trim().toLowerCase();
|
|
71
|
+
currentHead = oid.length > 0 ? oid : null;
|
|
62
72
|
}
|
|
63
73
|
else if (line.startsWith("branch ")) {
|
|
64
74
|
const ref = line.slice("branch ".length).trim();
|
|
@@ -82,6 +92,24 @@ function gitWorktreeList(repoRoot, git) {
|
|
|
82
92
|
}
|
|
83
93
|
return parseWorktreePorcelain(proc.stdout);
|
|
84
94
|
}
|
|
95
|
+
function resolveCommitOid(repoRoot, revision, git) {
|
|
96
|
+
let proc;
|
|
97
|
+
try {
|
|
98
|
+
proc = git(["rev-parse", "--verify", "--end-of-options", `${revision}^{commit}`], repoRoot);
|
|
99
|
+
}
|
|
100
|
+
catch (exc) {
|
|
101
|
+
throw new WorktreeMapConfigError(`could not resolve base '${revision}' to a commit OID in ${repoRoot}: ${String(exc)}`);
|
|
102
|
+
}
|
|
103
|
+
if (proc.returncode !== 0) {
|
|
104
|
+
throw new WorktreeMapConfigError(`could not resolve base '${revision}' to a commit OID in ${repoRoot} ` +
|
|
105
|
+
`(rc=${proc.returncode}): ${proc.stderr.trim() || "<no stderr>"}`);
|
|
106
|
+
}
|
|
107
|
+
const oid = (proc.stdout.trim().split(/\s+/)[0] ?? "").toLowerCase();
|
|
108
|
+
if (!/^[0-9a-f]{40,64}$/.test(oid)) {
|
|
109
|
+
throw new WorktreeMapConfigError(`git rev-parse for base '${revision}' did not return a commit OID: ${JSON.stringify(oid)}`);
|
|
110
|
+
}
|
|
111
|
+
return oid;
|
|
112
|
+
}
|
|
85
113
|
function createWorktree(repoRoot, worktreePath, baseBranch, git) {
|
|
86
114
|
mkdirSync(pathResolve(worktreePath, ".."), { recursive: true });
|
|
87
115
|
let proc;
|
|
@@ -96,7 +124,13 @@ function createWorktree(repoRoot, worktreePath, baseBranch, git) {
|
|
|
96
124
|
`(rc=${proc.returncode}): ${proc.stderr.trim() || "<no stderr>"}`);
|
|
97
125
|
}
|
|
98
126
|
}
|
|
99
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Resolve a story-to-worktree mapping into normalized C3 records (frozen contract).
|
|
129
|
+
*
|
|
130
|
+
* Registered paths are reused only when their porcelain HEAD OID matches the
|
|
131
|
+
* requested base OID. That comparison is a snapshot at resolution time — not a
|
|
132
|
+
* pin on the worker's start revision (`swarm:launch` emits a manifest and stops).
|
|
133
|
+
*/
|
|
100
134
|
export function resolveWorktreeMap(mapping, baseBranch, createMissing = true, options = {}) {
|
|
101
135
|
if (!Array.isArray(mapping)) {
|
|
102
136
|
throw new WorktreeMapConfigError(`worktree map must be a list of records, got ${typeof mapping}`);
|
|
@@ -154,14 +188,24 @@ export function resolveWorktreeMap(mapping, baseBranch, createMissing = true, op
|
|
|
154
188
|
});
|
|
155
189
|
}
|
|
156
190
|
const registered = gitWorktreeList(root, git);
|
|
191
|
+
const requestedOid = resolveCommitOid(root, trimmedBase, git);
|
|
157
192
|
for (const entry of resolved) {
|
|
158
|
-
|
|
193
|
+
const existing = registered.get(entry._key);
|
|
194
|
+
if (existing !== undefined) {
|
|
195
|
+
const actualOid = existing.head ?? "";
|
|
196
|
+
if (actualOid !== requestedOid) {
|
|
197
|
+
throw new WorktreeRevisionMismatchError(`registered worktree '${entry.worktree_path}' HEAD OID is ${actualOid || "(missing)"} ` +
|
|
198
|
+
`but requested base '${trimmedBase}' resolves to ${requestedOid}; ` +
|
|
199
|
+
`this is a snapshot check at resolution time, not a pin on the worker's start revision`);
|
|
200
|
+
}
|
|
201
|
+
ensureSubagentStatusDir(entry._abs);
|
|
159
202
|
continue;
|
|
160
203
|
}
|
|
161
204
|
if (!createMissing) {
|
|
162
205
|
throw new MissingWorktreeError(`story '${entry.story_id}' maps to '${entry.worktree_path}' which is not a registered git worktree and create_missing is disabled`);
|
|
163
206
|
}
|
|
164
207
|
createWorktree(root, entry._abs, trimmedBase, git);
|
|
208
|
+
ensureSubagentStatusDir(entry._abs);
|
|
165
209
|
}
|
|
166
210
|
return resolved.map(({ story_id, worktree_path, base_branch }) => ({
|
|
167
211
|
story_id,
|
|
@@ -38,6 +38,10 @@ export declare function reset(issueNumber: number, repo: string, deps: TriageAct
|
|
|
38
38
|
actor?: string | null;
|
|
39
39
|
projectRoot?: string;
|
|
40
40
|
}): string;
|
|
41
|
+
/** Success line for ``triage:accept``, plus the optional design-critique affordance (#3708). */
|
|
42
|
+
export declare function formatAcceptOutput(issueNumber: number, repo: string, decisionId: string, options?: {
|
|
43
|
+
autoPromote?: boolean;
|
|
44
|
+
}): string;
|
|
41
45
|
/** Format a decision entry for CLI ``status`` / ``history`` output. */
|
|
42
46
|
export declare function formatDecision(entry: AuditEntry | null): string;
|
|
43
47
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -407,6 +407,12 @@ export function reset(issueNumber, repo, deps, options = {}) {
|
|
|
407
407
|
});
|
|
408
408
|
return deps.candidatesLog.append(entry, { path: logPath });
|
|
409
409
|
}
|
|
410
|
+
/** Success line for ``triage:accept``, plus the optional design-critique affordance (#3708). */
|
|
411
|
+
export function formatAcceptOutput(issueNumber, repo, decisionId, options = {}) {
|
|
412
|
+
const promoteNote = options.autoPromote === true ? " + auto-promote" : "";
|
|
413
|
+
return (`accept #${issueNumber} (${repo}) -> ${decisionId}${promoteNote}\n` +
|
|
414
|
+
"optional: deft-directive-design-critique (declining writes nothing)");
|
|
415
|
+
}
|
|
410
416
|
/** Format a decision entry for CLI ``status`` / ``history`` output. */
|
|
411
417
|
export function formatDecision(entry) {
|
|
412
418
|
if (entry === null) {
|