@deftai/directive-core 0.106.0 → 0.107.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.
Files changed (100) hide show
  1. package/dist/check/gate-lists.js +2 -0
  2. package/dist/design-critique/exclusive-chip.d.ts +33 -0
  3. package/dist/design-critique/exclusive-chip.js +68 -0
  4. package/dist/design-critique/parent-audit.d.ts +59 -0
  5. package/dist/design-critique/parent-audit.js +121 -0
  6. package/dist/doctor/checks.d.ts +6 -0
  7. package/dist/doctor/checks.js +35 -0
  8. package/dist/hooks/dest-form.d.ts +46 -0
  9. package/dist/hooks/dest-form.js +575 -0
  10. package/dist/hooks/dispatcher.d.ts +17 -1
  11. package/dist/hooks/dispatcher.js +82 -11
  12. package/dist/hooks/index.d.ts +1 -0
  13. package/dist/hooks/index.js +1 -0
  14. package/dist/intake/github-auth-modes-cli.js +14 -2
  15. package/dist/intake/github-auth-modes.d.ts +54 -9
  16. package/dist/intake/github-auth-modes.js +326 -123
  17. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
  18. package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
  19. package/dist/lifecycle/completed-write-guard.d.ts +48 -0
  20. package/dist/lifecycle/completed-write-guard.js +373 -0
  21. package/dist/lifecycle/index.d.ts +1 -0
  22. package/dist/lifecycle/index.js +1 -0
  23. package/dist/literal-acceptance/capture.d.ts +16 -0
  24. package/dist/literal-acceptance/capture.js +75 -7
  25. package/dist/literal-acceptance/evaluate.js +25 -3
  26. package/dist/literal-acceptance/index.d.ts +2 -2
  27. package/dist/literal-acceptance/index.js +2 -2
  28. package/dist/literal-acceptance/run.js +9 -3
  29. package/dist/literal-acceptance/types.d.ts +6 -0
  30. package/dist/literal-acceptance/types.js +6 -0
  31. package/dist/policy/runtime-authority.d.ts +18 -0
  32. package/dist/policy/runtime-authority.js +11 -0
  33. package/dist/policy/write-fence.js +3 -0
  34. package/dist/preflight-cache/evaluate.js +9 -3
  35. package/dist/product-first-done-gate/evaluate.js +36 -5
  36. package/dist/scm/design-critique-chip.d.ts +37 -0
  37. package/dist/scm/design-critique-chip.js +171 -0
  38. package/dist/scm/index.d.ts +1 -0
  39. package/dist/scm/index.js +1 -0
  40. package/dist/scm/main.d.ts +3 -0
  41. package/dist/scm/main.js +15 -1
  42. package/dist/scm/readiness-cli.d.ts +2 -0
  43. package/dist/scm/readiness-cli.js +42 -1
  44. package/dist/scm/readiness.d.ts +2 -1
  45. package/dist/scm/readiness.js +7 -1
  46. package/dist/scope/decompose.js +2 -1
  47. package/dist/scope/delivery-evidence.d.ts +27 -1
  48. package/dist/scope/index.d.ts +1 -0
  49. package/dist/scope/index.js +1 -0
  50. package/dist/scope/lifecycle-write.d.ts +24 -0
  51. package/dist/scope/lifecycle-write.js +64 -0
  52. package/dist/scope/main.js +3 -1
  53. package/dist/scope/transition.js +90 -11
  54. package/dist/scope-provenance/evaluate.js +5 -1
  55. package/dist/session/git.d.ts +18 -0
  56. package/dist/session/git.js +81 -1
  57. package/dist/session/occupancy.d.ts +2 -1
  58. package/dist/session/occupancy.js +11 -4
  59. package/dist/session/session-start.d.ts +12 -0
  60. package/dist/session/session-start.js +17 -0
  61. package/dist/session/verify-session-ritual.d.ts +22 -3
  62. package/dist/session/verify-session-ritual.js +29 -4
  63. package/dist/swarm/launch.d.ts +69 -0
  64. package/dist/swarm/launch.js +191 -1
  65. package/dist/triage/actions/index.d.ts +4 -0
  66. package/dist/triage/actions/index.js +6 -0
  67. package/dist/triage/evaluate/evaluate.d.ts +11 -0
  68. package/dist/triage/evaluate/evaluate.js +169 -0
  69. package/dist/triage/evaluate/github.d.ts +16 -0
  70. package/dist/triage/evaluate/github.js +153 -0
  71. package/dist/triage/evaluate/index.d.ts +9 -0
  72. package/dist/triage/evaluate/index.js +8 -0
  73. package/dist/triage/evaluate/paths.d.ts +7 -0
  74. package/dist/triage/evaluate/paths.js +25 -0
  75. package/dist/triage/evaluate/sink.d.ts +5 -0
  76. package/dist/triage/evaluate/sink.js +52 -0
  77. package/dist/triage/evaluate/types.d.ts +93 -0
  78. package/dist/triage/evaluate/types.js +13 -0
  79. package/dist/triage/evaluate/validity.d.ts +8 -0
  80. package/dist/triage/evaluate/validity.js +87 -0
  81. package/dist/triage/evaluate/value.d.ts +12 -0
  82. package/dist/triage/evaluate/value.js +33 -0
  83. package/dist/triage/evaluate/wip-census.d.ts +5 -0
  84. package/dist/triage/evaluate/wip-census.js +39 -0
  85. package/dist/triage/evaluate/worktrees.d.ts +8 -0
  86. package/dist/triage/evaluate/worktrees.js +68 -0
  87. package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
  88. package/dist/triage/evaluate/xbrief-refs.js +60 -0
  89. package/dist/triage/help/registry-data.d.ts +21 -7
  90. package/dist/triage/help/registry-data.js +53 -5
  91. package/dist/triage/index.d.ts +1 -0
  92. package/dist/triage/index.js +1 -0
  93. package/dist/value/feedback-file.d.ts +17 -2
  94. package/dist/value/feedback-file.js +80 -6
  95. package/dist/vbrief-reconcile/labels.d.ts +1 -0
  96. package/dist/vbrief-reconcile/labels.js +30 -0
  97. package/dist/vbrief-validation/story-quality.d.ts +7 -0
  98. package/dist/vbrief-validation/story-quality.js +8 -1
  99. package/dist/xbrief-migrate/agents-header.js +69 -7
  100. package/package.json +3 -3
@@ -10,9 +10,10 @@ import { pythonJsonDump } from "./json.js";
10
10
  import { ENV_SESSION_POSTURE, readOnlyPostureMessage, resolveSessionPosture, ritualStateIsPostureAuthority, } from "./posture.js";
11
11
  import { defaultRitualRunner } from "./ritual-entrypoint.js";
12
12
  import { readRitualState, ritualStateMarksRearmNeeded, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
13
- import { formatSessionStartRecoveryCommand, GATED_STEPS, QUICK_STEPS, } from "./session-start.js";
13
+ import { formatSessionStartRecoveryCommand, GATED_STEPS, QUICK_STEPS, WRITE_GATED_EXECUTE_STEPS, WRITE_GATED_REQUIRED_STEPS, } from "./session-start.js";
14
14
  import { resolveSessionRitualStalenessHours } from "./staleness.js";
15
15
  export { formatCacheFetchAllRecoveryCommand, recoveryHintForStaleFailure, } from "./cache-recovery.js";
16
+ export { WRITE_GATED_EXECUTE_STEPS, WRITE_GATED_REQUIRED_STEPS };
16
17
  export const ENV_SKIP = "DEFT_SESSION_RITUAL_SKIP";
17
18
  export { ENTRYPOINT_TIMEOUT_EXIT_CODE, ENTRYPOINT_TIMEOUT_SECONDS, } from "./ritual-entrypoint.js";
18
19
  export const GATED_ENTRYPOINT_COMMANDS = {
@@ -244,7 +245,8 @@ function evaluateLoadedState(projectRoot, state, input) {
244
245
  }
245
246
  }
246
247
  if (input.tier === "gated") {
247
- for (const stepName of GATED_STEPS) {
248
+ const requiredGated = input.requiredGatedSteps ?? GATED_STEPS;
249
+ for (const stepName of requiredGated) {
248
250
  const step = state.gatedSteps[stepName];
249
251
  if (!stepPasses(step)) {
250
252
  return {
@@ -261,12 +263,31 @@ function evaluateLoadedState(projectRoot, state, input) {
261
263
  recoveryTier: null,
262
264
  };
263
265
  }
266
+ /**
267
+ * Options for the write/spawn mutation verifier (#3738).
268
+ * Required set omits `cache_fresh`; execution authority is `agent_hooks` only.
269
+ */
270
+ export function writeGateRitualOptions(extras = {}) {
271
+ return {
272
+ ...extras,
273
+ tier: "gated",
274
+ posture: extras.posture ?? "mutation",
275
+ requiredGatedSteps: WRITE_GATED_REQUIRED_STEPS,
276
+ executeGatedSteps: WRITE_GATED_EXECUTE_STEPS,
277
+ // Write dispatch already refuses skip (dispatcher passes bypass: false).
278
+ // Default the same here so CI DEFT_SESSION_RITUAL_SKIP=1 cannot empty
279
+ // the execute loop in tests that use this helper (#3738).
280
+ bypass: extras.bypass ?? false,
281
+ envSkip: extras.envSkip ?? "",
282
+ };
283
+ }
264
284
  /**
265
285
  * Read-only ritual-state inspection for host hooks.
266
286
  *
267
287
  * Unlike {@link verifySessionRitual}, this never runs missing gated entrypoints
268
- * and never rewrites `.deft/ritual-state.json`. A PreToolUse decision must be a
269
- * probe, not a hidden `doctor` / cache-refresh mutation boundary.
288
+ * and never rewrites `.deft/ritual-state.json`. Write dispatch does **not** use
289
+ * this function: it must still execute `agent_hooks` (#3738). A PreToolUse
290
+ * decision must not become a hidden `doctor` / cache-refresh mutation boundary.
270
291
  */
271
292
  export function inspectSessionRitual(projectRoot, options = {}) {
272
293
  const tier = options.tier ?? "quick";
@@ -442,7 +463,10 @@ export function verifySessionRitual(projectRoot, options = {}) {
442
463
  const runCmd = options.runner ?? defaultRitualRunner;
443
464
  const forced = new Set(options.forceGatedSteps ?? []);
444
465
  const detect = options.detectWorkSelection ?? detectWorkSelection;
466
+ const executable = new Set(options.executeGatedSteps ?? [...GATED_STEPS]);
445
467
  for (const stepName of GATED_STEPS) {
468
+ if (!executable.has(stepName))
469
+ continue;
446
470
  const step = gated[stepName];
447
471
  if (step?.deferred_reason && stepName !== "agent_hooks")
448
472
  continue;
@@ -519,6 +543,7 @@ export function verifySessionRitual(projectRoot, options = {}) {
519
543
  now: instant,
520
544
  runGit: options.runGit,
521
545
  rebindForwardHead: true,
546
+ requiredGatedSteps: options.requiredGatedSteps,
522
547
  });
523
548
  if (isBypassed) {
524
549
  return {
@@ -1,3 +1,4 @@
1
+ import { type ExpectedGithubWorkerPrincipal, type GhRunner, GITHUB_AUTH_MODE_HOST_GH, GITHUB_AUTH_MODE_INJECTED_TOKEN } from "../intake/github-auth-modes.js";
1
2
  import { EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK } from "./constants.js";
2
3
  import { type WorktreeRecord } from "./worktrees.js";
3
4
  export interface ResolvedStory {
@@ -21,6 +22,70 @@ export type RuntimeAuthProbeFn = () => [string, string];
21
22
  export declare const defaultPreflightGate: PreflightGateFn;
22
23
  export declare const defaultReadinessGate: ReadinessGateFn;
23
24
  export declare const defaultRuntimeAuthProbe: RuntimeAuthProbeFn;
25
+ export type WorkerCredentialDispatchPath = "grok-build" | "local-hybrid";
26
+ /**
27
+ * Dispatch-envelope half of the runtime / GitHub auth-mode contract (#1557 / #1351).
28
+ * Policy labels only — never a token value.
29
+ */
30
+ export declare function formatDispatchAuthEnvelope(fields: {
31
+ runtimeMode: string;
32
+ githubAuthMode: string;
33
+ expectedGithubLogin?: string | null;
34
+ }): string;
35
+ export interface WorkerCredentialInjectionRequest {
36
+ environ?: NodeJS.ProcessEnv;
37
+ githubAuthMode: string;
38
+ runtimeMode?: string | null;
39
+ dispatchPath?: WorkerCredentialDispatchPath;
40
+ expectedPrincipal?: ExpectedGithubWorkerPrincipal | null;
41
+ repo?: string;
42
+ runGh?: GhRunner;
43
+ cwd?: string;
44
+ }
45
+ export type WorkerCredentialInjectionResult = {
46
+ ok: true;
47
+ injected: true;
48
+ githubAuthMode: typeof GITHUB_AUTH_MODE_INJECTED_TOKEN;
49
+ runtimeMode: string | null;
50
+ expectedLogin: string;
51
+ spawnEnv: {
52
+ GH_TOKEN: string;
53
+ GITHUB_TOKEN: string;
54
+ GH_ENTERPRISE_TOKEN: string;
55
+ DEFT_EXPECTED_GITHUB_LOGIN: string;
56
+ };
57
+ envelopeSection: string;
58
+ } | {
59
+ ok: true;
60
+ injected: false;
61
+ githubAuthMode: typeof GITHUB_AUTH_MODE_HOST_GH;
62
+ runtimeMode: string | null;
63
+ expectedLogin: string | null;
64
+ spawnEnv: Record<string, never>;
65
+ envelopeSection: string;
66
+ } | {
67
+ ok: false;
68
+ blocked: true;
69
+ githubAuthMode: string;
70
+ runtimeMode: string | null;
71
+ missingCredential: string;
72
+ remedy: string;
73
+ detail: string;
74
+ failureKind: string | null;
75
+ envelopeSection: string;
76
+ };
77
+ /**
78
+ * Validate a held user-bearing worker credential and prepare spawn-time injection (#1351).
79
+ *
80
+ * Comparison is opt-in on the validator. This path always stamps
81
+ * `DEFT_EXPECTED_GITHUB_LOGIN` after a successful user-principal check so the
82
+ * worker envelope does not inherit "any user token is approved."
83
+ *
84
+ * Installation credentials fail closed via the existing validator (#3693).
85
+ * Token values belong only in the returned `spawnEnv` — never in prompts,
86
+ * transcripts, or manifest entries.
87
+ */
88
+ export declare function prepareWorkerCredentialInjection(request: WorkerCredentialInjectionRequest): WorkerCredentialInjectionResult;
24
89
  /**
25
90
  * True when a brief in `xbrief/completed/` (or the legacy `vbrief/completed/`)
26
91
  * references the given issue number in its `references[]` / `Traces`. Used by
@@ -74,6 +139,7 @@ export declare function buildManifest(resolved: readonly ResolvedStory[], option
74
139
  modelSource?: string | null;
75
140
  runtimeMode?: string | null;
76
141
  githubAuthMode?: string | null;
142
+ expectedGithubLogin?: string | null;
77
143
  occupancySessionId?: string | null;
78
144
  }): Record<string, unknown>[];
79
145
  export declare function orderCohort(resolved: readonly ResolvedStory[], _projectRoot: string): ResolvedStory[];
@@ -103,6 +169,9 @@ export interface LaunchArgs {
103
169
  * Defaults to `process.env` when unset.
104
170
  */
105
171
  environ?: NodeJS.ProcessEnv;
172
+ /** Optional gh runner for identity-bound credential injection (#1351). */
173
+ runGh?: GhRunner;
174
+ expectedPrincipal?: ExpectedGithubWorkerPrincipal | null;
106
175
  }
107
176
  export declare function swarmLaunch(args: LaunchArgs): {
108
177
  exitCode: number;
@@ -1,7 +1,7 @@
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";
@@ -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;
@@ -692,6 +854,33 @@ export function swarmLaunch(args) {
692
854
  stderr: `${occupancy.message}\n`,
693
855
  };
694
856
  }
857
+ const launchEnviron = args.environ ?? process.env;
858
+ let expectedGithubLogin = null;
859
+ // Only the injected-token launch path auto-validates (and fail-closes on a
860
+ // missing token). An ambient GH_TOKEN on a host-gh probe is often the
861
+ // maintainer workaround, not a worker credential; local-hybrid injection is
862
+ // the explicit prepareWorkerCredentialInjection call.
863
+ if (githubAuthMode === GITHUB_AUTH_MODE_INJECTED_TOKEN) {
864
+ const injection = prepareWorkerCredentialInjection({
865
+ environ: launchEnviron,
866
+ githubAuthMode,
867
+ runtimeMode,
868
+ dispatchPath: "grok-build",
869
+ runGh: args.runGh,
870
+ expectedPrincipal: args.expectedPrincipal,
871
+ cwd: projectRoot,
872
+ });
873
+ if (!injection.ok) {
874
+ return {
875
+ exitCode: EXIT_GATE_FAILED,
876
+ stdout: "",
877
+ stderr: `${injection.detail}\n${injection.remedy}\n`,
878
+ };
879
+ }
880
+ if (injection.injected) {
881
+ expectedGithubLogin = injection.expectedLogin;
882
+ }
883
+ }
695
884
  const manifest = buildManifest(ordered, {
696
885
  projectRoot,
697
886
  group: args.group ?? null,
@@ -708,6 +897,7 @@ export function swarmLaunch(args) {
708
897
  modelSource,
709
898
  runtimeMode,
710
899
  githubAuthMode,
900
+ expectedGithubLogin,
711
901
  occupancySessionId: occupancy.sessionId,
712
902
  });
713
903
  const rendered = `${JSON.stringify(manifest, null, 2)}\n`;
@@ -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) {
@@ -0,0 +1,11 @@
1
+ import { type EvaluateOptions, type EvaluateResult } from "./types.js";
2
+ export declare class EvaluateError extends Error {
3
+ name: string;
4
+ }
5
+ /**
6
+ * Stage A parent: WIP census + GitHub REST + worktree fan-out + sink write.
7
+ * Evaluators never receive the WIP census.
8
+ */
9
+ export declare function evaluateIssues(options: EvaluateOptions): Promise<EvaluateResult>;
10
+ export declare function renderEvaluateText(result: EvaluateResult): string;
11
+ //# sourceMappingURL=evaluate.d.ts.map
@@ -0,0 +1,169 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { runText } from "../../swarm/subprocess.js";
3
+ import { defaultGitRunner } from "../../swarm/worktrees.js";
4
+ import { defaultGithubReader, pullsMentioning } from "./github.js";
5
+ import { sha12Of, sinkDir } from "./paths.js";
6
+ import { gcStaleSha12Dirs, writeInvocationSink } from "./sink.js";
7
+ import { DEFAULT_CONCURRENCY, ORIGIN_MASTER, } from "./types.js";
8
+ import { evaluateValidity, joinValidityWithGithub } from "./validity.js";
9
+ import { buildValueAdvice } from "./value.js";
10
+ import { collectWipCensus, wipHitsForIssue } from "./wip-census.js";
11
+ import { addEvaluatorWorktree, removeEvaluatorWorktree } from "./worktrees.js";
12
+ export class EvaluateError extends Error {
13
+ name = "EvaluateError";
14
+ }
15
+ function resolveOriginSha(projectRoot, git) {
16
+ const proc = git(["rev-parse", ORIGIN_MASTER], projectRoot);
17
+ if (proc.returncode !== 0) {
18
+ throw new EvaluateError(`could not resolve ${ORIGIN_MASTER}: ${proc.stderr.trim() || "<no stderr>"}`);
19
+ }
20
+ return proc.stdout.trim();
21
+ }
22
+ function defaultSessionStart(worktreePath) {
23
+ const proc = runText(["deft", "session:start", "--read-only"], { cwd: worktreePath });
24
+ if (proc.returncode !== 0) {
25
+ throw new EvaluateError(`session:start --read-only failed in ${worktreePath}: ${proc.stderr.trim() || "<no stderr>"}`);
26
+ }
27
+ }
28
+ async function mapPool(items, limit, fn) {
29
+ const results = new Array(items.length);
30
+ let next = 0;
31
+ const worker = async () => {
32
+ while (true) {
33
+ const index = next;
34
+ next += 1;
35
+ if (index >= items.length) {
36
+ return;
37
+ }
38
+ results[index] = await fn(items[index], index);
39
+ }
40
+ };
41
+ const n = Math.max(1, Math.min(limit, Math.max(items.length, 1)));
42
+ await Promise.all(Array.from({ length: n }, () => worker()));
43
+ return results;
44
+ }
45
+ function collectGithubForRepo(repo, issues, reader) {
46
+ const views = issues.map((n) => reader.viewIssue(repo, n));
47
+ const openPulls = reader.listOpenPulls(repo);
48
+ reader.listOpenIssues(repo);
49
+ return { issues: views, openPulls };
50
+ }
51
+ /**
52
+ * Stage A parent: WIP census + GitHub REST + worktree fan-out + sink write.
53
+ * Evaluators never receive the WIP census.
54
+ */
55
+ export async function evaluateIssues(options) {
56
+ const issues = [...new Set(options.issues)].filter((n) => Number.isInteger(n) && n > 0);
57
+ if (issues.length === 0) {
58
+ throw new EvaluateError("triage:evaluate requires one or more issue numbers");
59
+ }
60
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
61
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
62
+ throw new EvaluateError("--concurrency must be a positive integer");
63
+ }
64
+ const git = options.git ?? defaultGitRunner;
65
+ const sessionStart = options.sessionStart ?? defaultSessionStart;
66
+ const github = options.github ?? defaultGithubReader();
67
+ const originSha = resolveOriginSha(options.projectRoot, git);
68
+ const sha12 = sha12Of(originSha);
69
+ const invocationId = options.invocationId ?? randomUUID();
70
+ const wip = collectWipCensus(options.projectRoot, issues);
71
+ const githubViews = collectGithubForRepo(options.repo, issues, github);
72
+ const viewByIssue = new Map(githubViews.issues.map((snap) => [snap.number, snap]));
73
+ const runOne = async (issue) => {
74
+ let worktreePath = null;
75
+ let teardownError = null;
76
+ const snap = viewByIssue.get(issue) ?? null;
77
+ const pulls = pullsMentioning(githubViews.openPulls, issue);
78
+ let verdict;
79
+ try {
80
+ worktreePath = addEvaluatorWorktree(options.projectRoot, issue, invocationId, originSha, git);
81
+ sessionStart(worktreePath);
82
+ const validity = evaluateValidity(worktreePath, issue);
83
+ const joined = joinValidityWithGithub(validity, snap?.state ?? null);
84
+ const duplicates = snap?.duplicateOf !== null && snap?.duplicateOf !== undefined ? [snap.duplicateOf] : [];
85
+ verdict = {
86
+ issue,
87
+ sha12,
88
+ invocationId,
89
+ validity: joined,
90
+ wip: wipHitsForIssue(wip, issue),
91
+ github: snap,
92
+ openPulls: pulls,
93
+ duplicates,
94
+ value: buildValueAdvice(snap),
95
+ error: null,
96
+ };
97
+ }
98
+ catch (err) {
99
+ const message = err instanceof Error ? err.message : String(err);
100
+ verdict = {
101
+ issue,
102
+ sha12,
103
+ invocationId,
104
+ validity: null,
105
+ wip: wipHitsForIssue(wip, issue),
106
+ github: snap,
107
+ openPulls: pulls,
108
+ duplicates: [],
109
+ value: buildValueAdvice(snap),
110
+ error: message,
111
+ };
112
+ }
113
+ finally {
114
+ if (worktreePath !== null) {
115
+ try {
116
+ removeEvaluatorWorktree(options.projectRoot, worktreePath, git);
117
+ }
118
+ catch (teardownErr) {
119
+ teardownError = teardownErr instanceof Error ? teardownErr.message : String(teardownErr);
120
+ }
121
+ }
122
+ }
123
+ if (teardownError !== null) {
124
+ verdict = {
125
+ ...verdict,
126
+ error: verdict.error !== null ? `${verdict.error}; teardown: ${teardownError}` : teardownError,
127
+ };
128
+ }
129
+ return verdict;
130
+ };
131
+ try {
132
+ const verdicts = await mapPool(issues, concurrency, (issue) => runOne(issue));
133
+ const result = {
134
+ sha12,
135
+ invocationId,
136
+ originSha,
137
+ sinkDir: sinkDir(options.projectRoot, sha12, invocationId),
138
+ concurrency,
139
+ verdicts,
140
+ };
141
+ writeInvocationSink(options.projectRoot, result);
142
+ gcStaleSha12Dirs(options.projectRoot, sha12);
143
+ return result;
144
+ }
145
+ catch (err) {
146
+ gcStaleSha12Dirs(options.projectRoot, sha12);
147
+ throw err;
148
+ }
149
+ }
150
+ export function renderEvaluateText(result) {
151
+ const lines = [
152
+ `triage:evaluate sha12=${result.sha12} invocation=${result.invocationId} concurrency=${result.concurrency}`,
153
+ `sink: ${result.sinkDir}`,
154
+ ];
155
+ for (const verdict of result.verdicts) {
156
+ const state = verdict.validity?.state ?? "error";
157
+ const evidence = verdict.validity?.evidence ?? verdict.error ?? "";
158
+ const wip = verdict.wip.length > 0 ? ` wip=${verdict.wip.length}` : " wip=0";
159
+ lines.push(`#${verdict.issue} validity=${state}${wip} critique-recommend: ${verdict.value["critique-recommend"]}`);
160
+ if (evidence.length > 0) {
161
+ lines.push(` ${evidence}`);
162
+ }
163
+ if (verdict.error !== null) {
164
+ lines.push(` error: ${verdict.error}`);
165
+ }
166
+ }
167
+ return `${lines.join("\n")}\n`;
168
+ }
169
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1,16 @@
1
+ import { type RunGhApiFn } from "../../scm/gh-rest.js";
2
+ import type { GithubCensus, GithubIssueSnapshot, GithubPullSnapshot } from "./types.js";
3
+ export declare function snapshotFromIssuePayload(raw: Record<string, unknown>): GithubIssueSnapshot;
4
+ export declare function snapshotFromPullPayload(raw: Record<string, unknown>): GithubPullSnapshot;
5
+ export interface GithubSeams {
6
+ readonly runGhApiFn?: RunGhApiFn;
7
+ }
8
+ /** GET-only GitHub census. Parent-owned. Never POST/PATCH/PUT/DELETE. */
9
+ export declare function collectGithubCensus(repo: string, issues: readonly number[], seams?: GithubSeams): GithubCensus;
10
+ export declare function defaultGithubReader(seams?: GithubSeams): {
11
+ viewIssue: (repo: string, n: number) => GithubIssueSnapshot;
12
+ listOpenIssues: (repo: string) => GithubIssueSnapshot[];
13
+ listOpenPulls: (repo: string) => GithubPullSnapshot[];
14
+ };
15
+ export declare function pullsMentioning(pulls: readonly GithubPullSnapshot[], issue: number): GithubPullSnapshot[];
16
+ //# sourceMappingURL=github.d.ts.map