@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
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { isAbsolute, resolve } from "node:path";
10
+ import { isInlineProseMention } from "./capture.js";
10
11
  import { evaluateCommandSafety, isExecutableLiteralSource } from "./safety.js";
11
12
  /** Default shell runner: exact command string via platform shell. */
12
13
  export function defaultLiteralAcceptanceRunner(input) {
@@ -95,7 +96,9 @@ export function runLiteralAcceptanceCommands(commands, options) {
95
96
  runs: [],
96
97
  };
97
98
  }
98
- const untrusted = commands.filter((c) => c.source === "task_statement" && options.allowTaskStatement !== true);
99
+ const untrusted = commands.filter((c) => c.source === "task_statement" &&
100
+ options.allowTaskStatement !== true &&
101
+ !isInlineProseMention(c));
99
102
  const executable = commands.filter((c) => isExecutableLiteralSource(c.source) ||
100
103
  (c.source === "task_statement" && options.allowTaskStatement === true));
101
104
  // Fail closed: every capture-only stated command must have a matching agent-promoted
@@ -149,8 +152,11 @@ export function runLiteralAcceptanceCommands(commands, options) {
149
152
  code: 1,
150
153
  message: `Literal acceptance-command gate FAILED (#3267): ${unpromoted.length} stated command(s) ` +
151
154
  `are capture-only (source=task_statement) and have no matching agent-promoted peer.\n` +
152
- `Promote the exact command strings (and cwd/expectedStdout/expectedExitCode when stated) ` +
153
- `into plan.metadata.swarm.verify_commands (or plan item / explicit metadata), then re-run.\n` +
155
+ `If the command is this story's acceptance, promote the exact strings ` +
156
+ `(and cwd/expectedStdout/expectedExitCode when stated) into ` +
157
+ `plan.metadata.swarm.verify_commands (or plan item / explicit metadata), then re-run.\n` +
158
+ `If the capture is not an acceptance command, record the exact string in ` +
159
+ `plan.metadata.literal_acceptance_not_commands without promoting it (#3721).\n` +
154
160
  `Note: an unrelated executable peer or a same-text peer with different context does not waive.\n` +
155
161
  listed,
156
162
  commands,
@@ -74,6 +74,12 @@ export interface LiteralAcceptanceGateResult {
74
74
  }
75
75
  /** Metadata key for operator-visible rejected command ledger (#3267). */
76
76
  export declare const LITERAL_ACCEPTANCE_REJECTED_METADATA_KEY: "literal_acceptance_rejected";
77
+ /**
78
+ * Metadata key: command strings the operator recorded as *not* acceptance
79
+ * commands (#3721). Disposition without promotion — listing a string here does
80
+ * not assert it is the story's verification.
81
+ */
82
+ export declare const LITERAL_ACCEPTANCE_NOT_COMMANDS_METADATA_KEY: "literal_acceptance_not_commands";
77
83
  /** Injectable shell runner for tests (default: spawnSync shell). */
78
84
  export type LiteralAcceptanceRunner = (input: {
79
85
  readonly command: string;
@@ -19,6 +19,12 @@ export const EXECUTABLE_LITERAL_SOURCES = [
19
19
  ];
20
20
  /** Metadata key for operator-visible rejected command ledger (#3267). */
21
21
  export const LITERAL_ACCEPTANCE_REJECTED_METADATA_KEY = "literal_acceptance_rejected";
22
+ /**
23
+ * Metadata key: command strings the operator recorded as *not* acceptance
24
+ * commands (#3721). Disposition without promotion — listing a string here does
25
+ * not assert it is the story's verification.
26
+ */
27
+ export const LITERAL_ACCEPTANCE_NOT_COMMANDS_METADATA_KEY = "literal_acceptance_not_commands";
22
28
  /** Metadata key on plan.metadata for captured literal commands (#3267). */
23
29
  export const LITERAL_ACCEPTANCE_METADATA_KEY = "literal_acceptance_commands";
24
30
  /** Alternate camelCase key accepted on read. */
@@ -11,6 +11,22 @@ export interface RuntimeAuthorityScopes {
11
11
  }
12
12
  /** Layers that contribute to a resolved write fence (#516 / #2443 / #2948 Wave 3). */
13
13
  export type WriteFenceSource = "project" | "story";
14
+ /**
15
+ * Whether recognized Shell dest-forms are enforced (#3438 / #3594).
16
+ *
17
+ * `off` (default) leaves Shell mutations exactly as they were before #3438:
18
+ * unrecognized and fail-open. `enforce` routes recognized dest-forms through
19
+ * `inspectMutationGates` and fail-closes targets it cannot prove.
20
+ *
21
+ * ! Default is `off` on purpose. Before #3438 Bash mutations were not gated at
22
+ * all, so defaulting to `enforce` would land new denials on every consumer with
23
+ * no opt-out. Opting IN via tracked project policy is a narrowing, which this
24
+ * fence model permits; a tracked switch that DISABLED enforcement would not be
25
+ * (see policy/deft-directive-disable.ts — repository-controlled content must
26
+ * not disable hooks for downstream clones).
27
+ */
28
+ export type ShellDestFormsMode = "off" | "enforce";
29
+ export declare const SHELL_DEST_FORMS_MODES: readonly ShellDestFormsMode[];
14
30
  export interface RuntimeAuthorityPolicy {
15
31
  readonly enabled: boolean;
16
32
  /** When non-empty, write targets must match at least one pattern. Empty = allow all paths. */
@@ -18,6 +34,8 @@ export interface RuntimeAuthorityPolicy {
18
34
  /** Deny wins over allow. */
19
35
  readonly denyPaths: readonly string[];
20
36
  readonly scopes: RuntimeAuthorityScopes;
37
+ /** Shell dest-form enforcement (#3438 / #3594). Default `off`. */
38
+ readonly shellDestForms: ShellDestFormsMode;
21
39
  /**
22
40
  * Optional story-layer allow globs from `plan.metadata.swarm.file_scope`
23
41
  * (populated by `resolveWriteFence` only). When non-empty, path must match
@@ -3,6 +3,7 @@ import { readPlanPolicy } from "./plan-extensions.js";
3
3
  import { loadProjectDefinition } from "./resolve.js";
4
4
  export const FIELD_RUNTIME_AUTHORITY = "plan.policy.runtimeAuthority";
5
5
  export const FIELD_RUNTIME_AUTHORITY_CLI_ALIAS = "runtimeAuthority";
6
+ export const SHELL_DEST_FORMS_MODES = ["off", "enforce"];
6
7
  export const DEFAULT_RUNTIME_AUTHORITY_SCOPES = {
7
8
  edits: true,
8
9
  push: false,
@@ -14,6 +15,7 @@ export const DEFAULT_RUNTIME_AUTHORITY_POLICY = {
14
15
  allowPaths: [],
15
16
  denyPaths: [],
16
17
  scopes: DEFAULT_RUNTIME_AUTHORITY_SCOPES,
18
+ shellDestForms: "off",
17
19
  };
18
20
  function readBoolean(rec, key, fallback) {
19
21
  if (key in rec && typeof rec[key] === "boolean") {
@@ -37,6 +39,11 @@ function readScopes(raw) {
37
39
  merge: readBoolean(rec, "merge", DEFAULT_RUNTIME_AUTHORITY_SCOPES.merge),
38
40
  };
39
41
  }
42
+ function readShellDestForms(raw) {
43
+ // Unknown / malformed resolves to the safe default rather than throwing, but
44
+ // validateRuntimeAuthority reports it so a typo is not silent (#3594).
45
+ return raw === "enforce" ? "enforce" : DEFAULT_RUNTIME_AUTHORITY_POLICY.shellDestForms;
46
+ }
40
47
  export function resolveRuntimeAuthorityPolicy(raw) {
41
48
  if (raw === null || raw === undefined) {
42
49
  return DEFAULT_RUNTIME_AUTHORITY_POLICY;
@@ -50,6 +57,7 @@ export function resolveRuntimeAuthorityPolicy(raw) {
50
57
  allowPaths: readStringArray(rec.allowPaths),
51
58
  denyPaths: readStringArray(rec.denyPaths),
52
59
  scopes: readScopes(rec.scopes),
60
+ shellDestForms: readShellDestForms(rec.shellDestForms),
53
61
  };
54
62
  }
55
63
  export function validateRuntimeAuthority(value) {
@@ -69,6 +77,9 @@ export function validateRuntimeAuthority(value) {
69
77
  errors.push(`${FIELD_RUNTIME_AUTHORITY}.${key} must be an array of path globs`);
70
78
  }
71
79
  }
80
+ if ("shellDestForms" in rec && !SHELL_DEST_FORMS_MODES.includes(rec.shellDestForms)) {
81
+ errors.push(`${FIELD_RUNTIME_AUTHORITY}.shellDestForms must be one of ${SHELL_DEST_FORMS_MODES.join(" | ")}`);
82
+ }
72
83
  if ("scopes" in rec) {
73
84
  const scopes = rec.scopes;
74
85
  if (typeof scopes !== "object" || scopes === null || Array.isArray(scopes)) {
@@ -140,6 +140,9 @@ export function resolveWriteFence(projectPolicy, storyFileScope, options) {
140
140
  allowPaths,
141
141
  denyPaths,
142
142
  scopes,
143
+ // Carry the project setting through: resolving a write fence must not
144
+ // silently enable Shell dest-form enforcement for a story (#3438 / #3594).
145
+ shellDestForms: projectPolicy.shellDestForms,
143
146
  storyAllowPaths: storyAllow,
144
147
  fenceSources: sources,
145
148
  };
@@ -300,13 +300,19 @@ function formatDriftFailure(repo, drift) {
300
300
  };
301
301
  }
302
302
  function runDriftProbe(resolvedRepo, cacheRoot, source, options) {
303
- if (options.probeDriftFn) {
304
- return options.probeDriftFn(resolvedRepo, cacheRoot, source);
305
- }
306
303
  try {
304
+ if (options.probeDriftFn) {
305
+ return options.probeDriftFn(resolvedRepo, cacheRoot, source);
306
+ }
307
307
  return probeCacheDrift({ repo: resolvedRepo, source, cacheRoot });
308
308
  }
309
309
  catch {
310
+ // Deliberate fail-open (#3738 / #3422): a thrown probe (429 after retry,
311
+ // 502/503, DNS, auth) returns null; evaluate treats null as pass.
312
+ // Work-selection / session:start still inherit this. The write path no
313
+ // longer executes cache_fresh, so this is not a write-authorization bypass.
314
+ // Do not silently convert to fail-closed without revisiting #3422 (local
315
+ // work MAY continue when it does not need the forge).
310
316
  return null;
311
317
  }
312
318
  }
@@ -10,7 +10,7 @@
10
10
  import { existsSync, readFileSync } from "node:fs";
11
11
  import { basename, isAbsolute, relative, resolve } from "node:path";
12
12
  import { emitAcceptanceStampFromPlan, MISSING_AMBIGUITY_ATTESTATION_CAUSE, stampedAmbiguityAttestationError, } from "../intake/clause-derivation.js";
13
- import { appendLiteralAcceptanceAdvisory, evaluateLiteralAcceptanceFromPlan, isNoopRefusalReason, runLiteralAcceptanceCommands, stripLiteralAcceptanceAdvisory, } from "../literal-acceptance/index.js";
13
+ import { appendLiteralAcceptanceAdvisory, evaluateLiteralAcceptanceFromPlan, isExecutableLiteralSource, isInlineProseMention, isNoopRefusalReason, readNotAcceptanceCommands, readStoredLiteralAcceptanceCommands, runLiteralAcceptanceCommands, stripLiteralAcceptanceAdvisory, } from "../literal-acceptance/index.js";
14
14
  import { ENV_RUN_SUMMARY_PATH, RunSummaryEmitter, } from "../run-summary/index.js";
15
15
  import { maybeBankOnAcPass } from "../session/ac-pass-banking.js";
16
16
  import { resolveAcReuse, resolveScopeIdForAcReuse, snapshotFromReuseFields, } from "../session/ac-pass-reuse.js";
@@ -205,9 +205,10 @@ export function evaluateVerifyAcFromPlan(plan, options = {}) {
205
205
  // the #3267 ledger is a parallel store and can be empty while stated commands exist.
206
206
  // Stated was previously excluded, so rung=stated + empty ledger printed "nothing to run".
207
207
  // Do not override a blocking rejected ledger or a config error.
208
- if (shouldRunPlanAcceptanceDirectly(base, acceptance)) {
208
+ if (shouldRunPlanAcceptanceDirectly(base, acceptance, plan)) {
209
209
  const runner = optionsWithScope.runner;
210
- const direct = runLiteralAcceptanceCommands(acceptance.commands.map((c) => ({
210
+ const directCommands = statedAcceptanceCommands(acceptance, plan);
211
+ const direct = runLiteralAcceptanceCommands(directCommands.map((c) => ({
211
212
  command: c.command,
212
213
  cwd: c.cwd ?? null,
213
214
  expectedStdout: c.expectedStdout ?? null,
@@ -271,8 +272,38 @@ export function evaluateVerifyAcFromPlan(plan, options = {}) {
271
272
  }
272
273
  return applyOracle(annotate(base, acceptance, optionsWithScope.quiet), optionsWithScope, plan);
273
274
  }
274
- function shouldRunPlanAcceptanceDirectly(base, acceptance) {
275
- if (acceptance.commands.length === 0)
275
+ function hasNonDefaultAcceptanceContext(command) {
276
+ const cwd = command.cwd !== undefined && command.cwd !== null && String(command.cwd).trim();
277
+ const stdout = command.expectedStdout !== undefined &&
278
+ command.expectedStdout !== null &&
279
+ String(command.expectedStdout).length > 0;
280
+ const exit = typeof command.expectedExitCode === "number" && command.expectedExitCode !== 0;
281
+ return Boolean(cwd) || stdout || exit;
282
+ }
283
+ function hasStructuredAcceptancePeer(plan, command) {
284
+ return readStoredLiteralAcceptanceCommands(plan).some((c) => c.command === command && isExecutableLiteralSource(c.source));
285
+ }
286
+ /**
287
+ * True when a plan.acceptance.commands row is only an ingest copy of an inline
288
+ * prose mention (or an operator not-command disposition) and has no structured
289
+ * peer. Independently authored rows — promoted verify_commands, or a command
290
+ * with its own cwd/expectedStdout/exit — still run (#3721 Greptile).
291
+ */
292
+ function isInlineScrapedAcceptanceCommand(plan, command) {
293
+ if (hasNonDefaultAcceptanceContext(command))
294
+ return false;
295
+ if (hasStructuredAcceptancePeer(plan, command.command))
296
+ return false;
297
+ if (readNotAcceptanceCommands(plan).has(command.command))
298
+ return true;
299
+ return readStoredLiteralAcceptanceCommands(plan).some((c) => c.command === command.command && isInlineProseMention(c));
300
+ }
301
+ /** plan.acceptance.commands minus ingest-copied inline mentions (#3721). */
302
+ function statedAcceptanceCommands(acceptance, plan) {
303
+ return acceptance.commands.filter((c) => !isInlineScrapedAcceptanceCommand(plan, c));
304
+ }
305
+ function shouldRunPlanAcceptanceDirectly(base, acceptance, plan) {
306
+ if (statedAcceptanceCommands(acceptance, plan).length === 0)
276
307
  return false;
277
308
  if (base.runs.length > 0)
278
309
  return false;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Chip-only remaining-set write for design-critique catalog labels (#3642).
3
+ *
4
+ * Parent attach of triage-ready / recut mechanism-shaped MUST use this verb,
5
+ * not `gh api POST .../labels` and not additive `scm:issue:edit --add-label`.
6
+ */
7
+ import { type DesignCritiqueCatalogChip } from "../design-critique/exclusive-chip.js";
8
+ import type { LabelClient } from "../vbrief-reconcile/types.js";
9
+ export declare const DESIGN_CRITIQUE_CHIP_VERB: "design-critique-chip";
10
+ export declare const DESIGN_CRITIQUE_CHIP_USAGE: string;
11
+ export interface DesignCritiqueChipArgs {
12
+ readonly issue: number;
13
+ readonly chip: DesignCritiqueCatalogChip;
14
+ readonly repo: string | null;
15
+ readonly json: boolean;
16
+ }
17
+ export interface DesignCritiqueChipSeams {
18
+ readonly client?: LabelClient;
19
+ /** Default OWNER/NAME when --repo is omitted (git origin). */
20
+ readonly resolveDefaultRepo?: () => string | null;
21
+ }
22
+ export interface DesignCritiqueChipResult {
23
+ readonly exitCode: number;
24
+ readonly stdout: string;
25
+ readonly stderr: string;
26
+ }
27
+ /** Fail-closed chip name. Closed catalog only. */
28
+ export declare function resolveDesignCritiqueChipArg(raw: string): DesignCritiqueCatalogChip;
29
+ export declare function parseDesignCritiqueChipArgs(extra: readonly string[]): DesignCritiqueChipArgs;
30
+ /** Resolve OWNER/NAME from `git remote get-url origin`. */
31
+ export declare function resolveRepoFromGitOrigin(): string | null;
32
+ /**
33
+ * GET current labels, remaining-set replace via applyDesignCritiqueCatalogChip.
34
+ * One LabelClient.apply write. Other facets stay.
35
+ */
36
+ export declare function runDesignCritiqueChip(extra: readonly string[], seams?: DesignCritiqueChipSeams): DesignCritiqueChipResult;
37
+ //# sourceMappingURL=design-critique-chip.d.ts.map
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Chip-only remaining-set write for design-critique catalog labels (#3642).
3
+ *
4
+ * Parent attach of triage-ready / recut mechanism-shaped MUST use this verb,
5
+ * not `gh api POST .../labels` and not additive `scm:issue:edit --add-label`.
6
+ */
7
+ import { spawnSync } from "node:child_process";
8
+ import { applyDesignCritiqueCatalogChip, } from "../design-critique/exclusive-chip.js";
9
+ import { parseGithubOwnerRepo } from "../policy/sync-default.js";
10
+ import { ScmLabelClient, ScmLabelError } from "../vbrief-reconcile/labels.js";
11
+ import { extractFlag, extractValueFlag } from "./argv.js";
12
+ import { InvalidRepoError, splitRepo } from "./gh-rest.js";
13
+ import { pyRepr } from "./py-format.js";
14
+ export const DESIGN_CRITIQUE_CHIP_VERB = "design-critique-chip";
15
+ export const DESIGN_CRITIQUE_CHIP_USAGE = "usage: scm issue design-critique-chip --issue N --chip triage-ready|mechanism-shaped [--repo OWNER/NAME] [--json]\n" +
16
+ " Parent attach of design-critique:triage-ready / recut mechanism-shaped.\n" +
17
+ " Closed catalog remaining-set replace. One write. Other facets stay.\n";
18
+ const CHIP_ALIASES = {
19
+ "triage-ready": "design-critique:triage-ready",
20
+ "mechanism-shaped": "design-critique:mechanism-shaped",
21
+ "design-critique:triage-ready": "design-critique:triage-ready",
22
+ "design-critique:mechanism-shaped": "design-critique:mechanism-shaped",
23
+ };
24
+ /** Fail-closed chip name. Closed catalog only. */
25
+ export function resolveDesignCritiqueChipArg(raw) {
26
+ const chip = CHIP_ALIASES[raw];
27
+ if (chip === undefined) {
28
+ throw new Error(`unknown design-critique chip ${pyRepr(raw)}; expected triage-ready|mechanism-shaped ` +
29
+ "(or design-critique:triage-ready|design-critique:mechanism-shaped)");
30
+ }
31
+ return chip;
32
+ }
33
+ function parseIssueNumber(raw, source) {
34
+ const issueN = Number.parseInt(raw, 10);
35
+ if (Number.isNaN(issueN) || issueN <= 0 || String(issueN) !== raw) {
36
+ throw new Error(`${source} must be a positive integer; got ${pyRepr(raw)}`);
37
+ }
38
+ return issueN;
39
+ }
40
+ export function parseDesignCritiqueChipArgs(extra) {
41
+ let remainder = [...extra];
42
+ const [help] = extractFlag(remainder, "--help");
43
+ const [helpShort] = extractFlag(remainder, "-h");
44
+ if (help || helpShort) {
45
+ throw new ChipUsageError(DESIGN_CRITIQUE_CHIP_USAGE.trimEnd());
46
+ }
47
+ const [json, afterJson] = extractFlag(remainder, "--json");
48
+ remainder = afterJson;
49
+ const [repoRaw, afterRepo] = extractValueFlag(remainder, "--repo");
50
+ remainder = afterRepo;
51
+ const [chipRaw, afterChip] = extractValueFlag(remainder, "--chip");
52
+ remainder = afterChip;
53
+ const [issueFlag, afterIssue] = extractValueFlag(remainder, "--issue");
54
+ remainder = afterIssue;
55
+ const leftoverFlags = remainder.filter((t) => t.startsWith("-"));
56
+ if (leftoverFlags.length > 0) {
57
+ throw new Error(`unrecognized flags: ${pyRepr(leftoverFlags)}. Supported: --issue, --chip, --repo, --json.`);
58
+ }
59
+ const positionals = remainder.filter((t) => !t.startsWith("-"));
60
+ if (chipRaw === null || chipRaw.length === 0) {
61
+ throw new Error("missing --chip triage-ready|mechanism-shaped");
62
+ }
63
+ const chip = resolveDesignCritiqueChipArg(chipRaw);
64
+ let issueRaw = issueFlag;
65
+ if (positionals.length > 1) {
66
+ throw new Error(`expected at most one positional issue number; got ${pyRepr(positionals)}`);
67
+ }
68
+ if (positionals.length === 1) {
69
+ const positional = positionals[0] ?? "";
70
+ if (issueRaw !== null && issueRaw !== positional) {
71
+ throw new Error(`--issue ${pyRepr(issueRaw)} conflicts with positional ${pyRepr(positional)}`);
72
+ }
73
+ issueRaw = positional;
74
+ }
75
+ if (issueRaw === null || issueRaw.length === 0) {
76
+ throw new Error("missing --issue N");
77
+ }
78
+ const issue = parseIssueNumber(issueRaw, "--issue");
79
+ if (repoRaw !== null && repoRaw.length > 0) {
80
+ splitRepo(repoRaw);
81
+ }
82
+ return { issue, chip, repo: repoRaw !== null && repoRaw.length > 0 ? repoRaw : null, json };
83
+ }
84
+ /** Resolve OWNER/NAME from `git remote get-url origin`. */
85
+ export function resolveRepoFromGitOrigin() {
86
+ const result = spawnSync("git", ["remote", "get-url", "origin"], {
87
+ encoding: "utf8",
88
+ env: process.env,
89
+ });
90
+ if (result.status !== 0)
91
+ return null;
92
+ const stdout = typeof result.stdout === "string" ? result.stdout : "";
93
+ return parseGithubOwnerRepo(stdout);
94
+ }
95
+ class ChipUsageError extends Error {
96
+ constructor(message) {
97
+ super(message);
98
+ this.name = "ChipUsageError";
99
+ }
100
+ }
101
+ /**
102
+ * GET current labels, remaining-set replace via applyDesignCritiqueCatalogChip.
103
+ * One LabelClient.apply write. Other facets stay.
104
+ */
105
+ export function runDesignCritiqueChip(extra, seams = {}) {
106
+ let args;
107
+ try {
108
+ args = parseDesignCritiqueChipArgs(extra);
109
+ }
110
+ catch (err) {
111
+ if (err instanceof ChipUsageError) {
112
+ return { exitCode: 0, stdout: `${err.message}\n`, stderr: "" };
113
+ }
114
+ if (err instanceof InvalidRepoError) {
115
+ return { exitCode: 2, stdout: "", stderr: `error: invalid --repo value: ${err.message}\n` };
116
+ }
117
+ const message = err instanceof Error ? err.message : String(err);
118
+ return { exitCode: 2, stdout: "", stderr: `error: ${message}\n` };
119
+ }
120
+ const repo = args.repo ?? (seams.resolveDefaultRepo ?? resolveRepoFromGitOrigin)();
121
+ if (repo === null || repo.length === 0) {
122
+ return {
123
+ exitCode: 2,
124
+ stdout: "",
125
+ stderr: "error: missing --repo OWNER/NAME (could not resolve from git origin)\n",
126
+ };
127
+ }
128
+ try {
129
+ splitRepo(repo);
130
+ }
131
+ catch (err) {
132
+ if (err instanceof InvalidRepoError) {
133
+ return { exitCode: 2, stdout: "", stderr: `error: invalid --repo value: ${err.message}\n` };
134
+ }
135
+ throw err;
136
+ }
137
+ const client = seams.client ?? new ScmLabelClient();
138
+ try {
139
+ const applied = applyDesignCritiqueCatalogChip(client, repo, args.issue, args.chip);
140
+ const payload = {
141
+ repo,
142
+ issue: args.issue,
143
+ chip: args.chip,
144
+ add: [...applied.add],
145
+ remove: [...applied.remove],
146
+ remaining: [...applied.remaining],
147
+ };
148
+ if (args.json) {
149
+ return { exitCode: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: "" };
150
+ }
151
+ const wrote = applied.add.length > 0 || applied.remove.length > 0;
152
+ const detail = applied.remove.length > 0
153
+ ? `removed ${applied.remove.join(", ")}`
154
+ : wrote
155
+ ? "added"
156
+ : "already exclusive";
157
+ return {
158
+ exitCode: 0,
159
+ stdout: `${wrote ? "applied" : "unchanged"} ${args.chip} on ${repo}#${args.issue} (${detail})\n`,
160
+ stderr: "",
161
+ };
162
+ }
163
+ catch (err) {
164
+ if (err instanceof ScmLabelError) {
165
+ return { exitCode: 1, stdout: "", stderr: `error: ${err.message}\n` };
166
+ }
167
+ const message = err instanceof Error ? err.message : String(err);
168
+ return { exitCode: 1, stdout: "", stderr: `error: ${message}\n` };
169
+ }
170
+ }
171
+ //# sourceMappingURL=design-critique-chip.js.map
@@ -3,6 +3,7 @@ export * from "./binary.js";
3
3
  export * from "./build-command.js";
4
4
  export * from "./call.js";
5
5
  export * from "./constants.js";
6
+ export * from "./design-critique-chip.js";
6
7
  export * from "./errors.js";
7
8
  export * from "./gh-rest.js";
8
9
  export * from "./main.js";
package/dist/scm/index.js CHANGED
@@ -3,6 +3,7 @@ export * from "./binary.js";
3
3
  export * from "./build-command.js";
4
4
  export * from "./call.js";
5
5
  export * from "./constants.js";
6
+ export * from "./design-critique-chip.js";
6
7
  export * from "./errors.js";
7
8
  export * from "./gh-rest.js";
8
9
  export * from "./main.js";
@@ -1,3 +1,4 @@
1
+ import type { LabelClient } from "../vbrief-reconcile/types.js";
1
2
  import type { GhRestSeams } from "./gh-rest.js";
2
3
  export interface MainOptions {
3
4
  readonly whichFn?: Parameters<typeof import("./binary.js").resolveBinary>[0];
@@ -8,6 +9,8 @@ export interface MainOptions {
8
9
  * Production CLI always probes.
9
10
  */
10
11
  readonly skipReadiness?: boolean;
12
+ /** LabelClient seam for `issue design-critique-chip` (#3642). */
13
+ readonly labelClient?: LabelClient;
11
14
  }
12
15
  /**
13
16
  * CLI entry point. Returns the underlying binary's exit code (or 2 on arg error).
package/dist/scm/main.js CHANGED
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { extractFlag } from "./argv.js";
3
3
  import { buildCommand } from "./build-command.js";
4
4
  import { REST_OPT_IN_VERBS } from "./constants.js";
5
+ import { DESIGN_CRITIQUE_CHIP_VERB, runDesignCritiqueChip } from "./design-critique-chip.js";
5
6
  import { ScmStubError } from "./errors.js";
6
7
  import { requireScmReady } from "./readiness.js";
7
8
  import { runRestList, runRestView } from "./rest-dispatch.js";
@@ -30,13 +31,26 @@ function guardScmReady(options) {
30
31
  export function main(argv, options = {}) {
31
32
  if (argv.length < 2) {
32
33
  process.stderr.write("usage: scm.py <namespace> <verb> [pass-through args...]\n" +
33
- " (v1 stub: namespace=issue, verb=list|view|close|edit)\n" +
34
+ " (v1 stub: namespace=issue, verb=list|view|close|edit|design-critique-chip)\n" +
34
35
  " --rest opt-in is supported on issue view/list (#976)\n");
35
36
  return 2;
36
37
  }
37
38
  const namespace = argv[0] ?? "";
38
39
  const verb = argv[1] ?? "";
39
40
  let extra = argv.slice(2);
41
+ if (namespace === "issue" && verb === DESIGN_CRITIQUE_CHIP_VERB) {
42
+ const blocked = guardScmReady(options);
43
+ if (blocked !== null)
44
+ return blocked;
45
+ const result = runDesignCritiqueChip(extra, { client: options.labelClient });
46
+ if (result.stdout.length > 0) {
47
+ process.stdout.write(result.stdout);
48
+ }
49
+ if (result.stderr.length > 0) {
50
+ process.stderr.write(result.stderr);
51
+ }
52
+ return result.exitCode;
53
+ }
40
54
  const [restMode, afterRest] = extractFlag(extra, "--rest");
41
55
  extra = afterRest;
42
56
  if (restMode) {
@@ -10,6 +10,8 @@ export interface ScmReadinessCliArgs {
10
10
  readonly deep?: boolean;
11
11
  readonly depth?: ScmProbeDepth;
12
12
  readonly help?: boolean;
13
+ readonly repo?: string;
14
+ readonly expectedLogin?: string;
13
15
  }
14
16
  export declare function parseScmReadinessArgs(argv: readonly string[]): {
15
17
  args: ScmReadinessCliArgs;
@@ -5,6 +5,7 @@
5
5
  * Exit 0 when ready; exit 1 when not ready (loud diagnostic); exit 2 on bad flags.
6
6
  */
7
7
  import { fileURLToPath } from "node:url";
8
+ import { INSTALLATION_IDENTITY_ISSUE_URL, PRINCIPAL_KIND_USER, } from "../intake/github-auth-modes.js";
8
9
  import { clearScmReadyCache, formatScmReadinessLines, probeScmReadiness, scmReadinessToDict, } from "./readiness.js";
9
10
  export function parseScmReadinessArgs(argv) {
10
11
  const out = {};
@@ -30,6 +31,26 @@ export function parseScmReadinessArgs(argv) {
30
31
  }
31
32
  out.depth = value;
32
33
  }
34
+ else if (arg === "--repo") {
35
+ const value = argv[++i];
36
+ if (value === undefined || value.startsWith("-")) {
37
+ return { args: out, error: "--repo expects owner/repo" };
38
+ }
39
+ out.repo = value;
40
+ }
41
+ else if (arg === "--expected-login") {
42
+ const value = argv[++i];
43
+ if (value === undefined || value.startsWith("-")) {
44
+ return { args: out, error: "--expected-login expects a GitHub login" };
45
+ }
46
+ out.expectedLogin = value;
47
+ }
48
+ else if (arg === "--expected-app-slug" || arg === "--expected-installation-id") {
49
+ return {
50
+ args: out,
51
+ error: `${arg} is not accepted; GitHub App installation identity is deferred to ${INSTALLATION_IDENTITY_ISSUE_URL}`,
52
+ };
53
+ }
33
54
  else if (arg === "--help" || arg === "-h") {
34
55
  out.help = true;
35
56
  }
@@ -43,8 +64,19 @@ export function parseScmReadinessArgs(argv) {
43
64
  return { args: out, error: null };
44
65
  }
45
66
  const USAGE = "usage: deft scm:status [--json] [--deep|--shallow|--depth shallow|deep]\n" +
67
+ " [--repo OWNER/REPO] [--expected-login LOGIN]\n" +
46
68
  " Report SCM (gh/ghx) availability + auth state in this execution env (#2275).\n" +
69
+ " Deep probes derive the target repo (flag / GH_REPO / GITHUB_REPOSITORY / origin)\n" +
70
+ " and compare an expected user login when one is supplied (#3665).\n" +
71
+ " GitHub App installation identity is deferred to #3693.\n" +
47
72
  " Exit 0 ready / 1 not ready / 2 config error.\n";
73
+ function expectedPrincipalFromArgs(args) {
74
+ const login = args.expectedLogin?.trim() ?? "";
75
+ if (login.length > 0) {
76
+ return { kind: PRINCIPAL_KIND_USER, login };
77
+ }
78
+ return undefined;
79
+ }
48
80
  export function scmReadinessMain(args, options = {}) {
49
81
  const writeOut = options.writeOut ?? ((s) => process.stdout.write(s));
50
82
  const writeErr = options.writeErr ?? ((s) => process.stderr.write(s));
@@ -52,12 +84,21 @@ export function scmReadinessMain(args, options = {}) {
52
84
  writeOut(USAGE);
53
85
  return 0;
54
86
  }
87
+ const expectedPrincipal = expectedPrincipalFromArgs(args);
88
+ if (expectedPrincipal !== undefined && "error" in expectedPrincipal) {
89
+ writeErr(`error: ${expectedPrincipal.error}\n${USAGE}`);
90
+ return 2;
91
+ }
55
92
  const depth = args.depth ?? (args.deep ? "deep" : "shallow");
56
93
  // Explicit probe always re-evaluates (credentials may have changed since
57
94
  // session:start). Clears the process-scoped requireScmReady cache too.
58
95
  clearScmReadyCache();
59
96
  const probe = options.probe ?? probeScmReadiness;
60
- const report = probe({ depth });
97
+ const report = probe({
98
+ depth,
99
+ repo: args.repo,
100
+ expectedPrincipal,
101
+ });
61
102
  if (args.json) {
62
103
  writeOut(`${JSON.stringify(scmReadinessToDict(report), null, 2)}\n`);
63
104
  }
@@ -18,7 +18,7 @@
18
18
  * env token presence + optional short `gh auth status`). Deep API validation
19
19
  * is opt-in via `deep: true` (session:start --with-network).
20
20
  */
21
- import { findInjectedToken, type GhRunner, GITHUB_AUTH_MODE_HOST_GH, GITHUB_AUTH_MODE_INJECTED_TOKEN } from "../intake/github-auth-modes.js";
21
+ import { type ExpectedGithubWorkerPrincipal, findInjectedToken, type GhRunner, GITHUB_AUTH_MODE_HOST_GH, GITHUB_AUTH_MODE_INJECTED_TOKEN } from "../intake/github-auth-modes.js";
22
22
  import { type RuntimeCapabilityReport } from "../intake/platform-capabilities.js";
23
23
  import { type WhichFn } from "./binary.js";
24
24
  import { BINARY_PREFERENCE } from "./constants.js";
@@ -64,6 +64,7 @@ export interface ProbeScmReadinessOptions {
64
64
  readonly runtimeReport?: RuntimeCapabilityReport;
65
65
  readonly githubAuthMode?: string | null;
66
66
  readonly repo?: string;
67
+ readonly expectedPrincipal?: ExpectedGithubWorkerPrincipal | null;
67
68
  readonly runGh?: GhRunner;
68
69
  /** When false, skip even `gh auth status` on the shallow path (tests / pure PATH). */
69
70
  readonly checkAuthStatus?: boolean;
@@ -105,7 +105,12 @@ function mapDeepFailureToAuthState(result) {
105
105
  return "missing-token";
106
106
  if (result.failureKind === "gh_auth_failed")
107
107
  return "unauthenticated";
108
- if (result.failureKind === "api_unreachable" || result.failureKind === "repo_access_denied") {
108
+ if (result.failureKind === "api_unreachable" ||
109
+ result.failureKind === "repo_access_denied" ||
110
+ result.failureKind === "missing_expected_principal" ||
111
+ result.failureKind === "principal_mismatch" ||
112
+ result.failureKind === "missing_target_repo" ||
113
+ result.failureKind === "installation_identity_unverifiable") {
109
114
  return "unauthenticated";
110
115
  }
111
116
  return "unknown";
@@ -192,6 +197,7 @@ export function probeScmReadiness(options = {}) {
192
197
  environ: env,
193
198
  runtimeReport,
194
199
  repo: options.repo,
200
+ expectedPrincipal: options.expectedPrincipal,
195
201
  runGh: deepRunner,
196
202
  });
197
203
  if (deep.ok) {