@deftai/directive-core 0.105.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 (173) hide show
  1. package/dist/authz/classify.js +591 -34
  2. package/dist/check/gate-lists.js +2 -0
  3. package/dist/check/named-cause.js +1 -1
  4. package/dist/design-critique/exclusive-chip.d.ts +33 -0
  5. package/dist/design-critique/exclusive-chip.js +68 -0
  6. package/dist/design-critique/parent-audit.d.ts +59 -0
  7. package/dist/design-critique/parent-audit.js +121 -0
  8. package/dist/doctor/checks.d.ts +6 -0
  9. package/dist/doctor/checks.js +35 -0
  10. package/dist/forward-coverage/diff-coverage.d.ts +39 -0
  11. package/dist/forward-coverage/diff-coverage.js +112 -0
  12. package/dist/forward-coverage/diff-lines.d.ts +19 -0
  13. package/dist/forward-coverage/diff-lines.js +173 -0
  14. package/dist/forward-coverage/evaluate.d.ts +24 -9
  15. package/dist/forward-coverage/evaluate.js +125 -19
  16. package/dist/hooks/dest-form.d.ts +46 -0
  17. package/dist/hooks/dest-form.js +575 -0
  18. package/dist/hooks/dispatcher.d.ts +17 -1
  19. package/dist/hooks/dispatcher.js +82 -11
  20. package/dist/hooks/index.d.ts +1 -0
  21. package/dist/hooks/index.js +1 -0
  22. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  23. package/dist/init-deposit/agent-hooks.js +1 -1
  24. package/dist/intake/clause-derivation.d.ts +9 -0
  25. package/dist/intake/clause-derivation.js +30 -3
  26. package/dist/intake/github-auth-modes-cli.js +14 -2
  27. package/dist/intake/github-auth-modes.d.ts +54 -9
  28. package/dist/intake/github-auth-modes.js +326 -123
  29. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
  30. package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
  31. package/dist/lifecycle/completed-write-guard.d.ts +48 -0
  32. package/dist/lifecycle/completed-write-guard.js +373 -0
  33. package/dist/lifecycle/index.d.ts +1 -0
  34. package/dist/lifecycle/index.js +1 -0
  35. package/dist/lifecycle-visible/evaluate.d.ts +98 -0
  36. package/dist/lifecycle-visible/evaluate.js +710 -0
  37. package/dist/lifecycle-visible/index.d.ts +2 -0
  38. package/dist/lifecycle-visible/index.js +2 -0
  39. package/dist/literal-acceptance/capture.d.ts +16 -0
  40. package/dist/literal-acceptance/capture.js +78 -10
  41. package/dist/literal-acceptance/evaluate.js +25 -3
  42. package/dist/literal-acceptance/index.d.ts +2 -2
  43. package/dist/literal-acceptance/index.js +2 -2
  44. package/dist/literal-acceptance/run.js +9 -3
  45. package/dist/literal-acceptance/types.d.ts +6 -0
  46. package/dist/literal-acceptance/types.js +6 -0
  47. package/dist/orchestration/judgment-policy.d.ts +13 -0
  48. package/dist/orchestration/judgment-policy.js +15 -0
  49. package/dist/orchestration/probe-session.js +13 -8
  50. package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
  51. package/dist/orchestration/verify-judgment-gates.js +43 -11
  52. package/dist/policy/ceremony-dial.js +7 -7
  53. package/dist/policy/host-hooks.d.ts +20 -0
  54. package/dist/policy/host-hooks.js +121 -2
  55. package/dist/policy/org-force-on-migration.js +2 -1
  56. package/dist/policy/plan-extensions.d.ts +1 -1
  57. package/dist/policy/plan-extensions.js +2 -0
  58. package/dist/policy/product-signal.js +6 -7
  59. package/dist/policy/require-human-merge.js +8 -6
  60. package/dist/policy/resolve.d.ts +7 -2
  61. package/dist/policy/resolve.js +25 -8
  62. package/dist/policy/runtime-authority.d.ts +18 -0
  63. package/dist/policy/runtime-authority.js +11 -0
  64. package/dist/policy/value-feedback.js +7 -8
  65. package/dist/policy/write-fence.js +3 -0
  66. package/dist/preflight-cache/evaluate.d.ts +12 -0
  67. package/dist/preflight-cache/evaluate.js +24 -4
  68. package/dist/preflight-cache/index.d.ts +1 -1
  69. package/dist/preflight-cache/index.js +1 -1
  70. package/dist/product-first-done-gate/evaluate.d.ts +4 -0
  71. package/dist/product-first-done-gate/evaluate.js +82 -10
  72. package/dist/release/closed-verb-gate.d.ts +12 -0
  73. package/dist/release/closed-verb-gate.js +51 -0
  74. package/dist/release/index.d.ts +1 -0
  75. package/dist/release/index.js +1 -0
  76. package/dist/release/pipeline.js +7 -0
  77. package/dist/release/types.d.ts +12 -0
  78. package/dist/render/constants.d.ts +2 -1
  79. package/dist/render/constants.js +2 -1
  80. package/dist/render/framework-commands.js +4 -0
  81. package/dist/render/spec-validate.js +23 -12
  82. package/dist/run-summary/types.d.ts +4 -0
  83. package/dist/scm/design-critique-chip.d.ts +37 -0
  84. package/dist/scm/design-critique-chip.js +171 -0
  85. package/dist/scm/index.d.ts +1 -0
  86. package/dist/scm/index.js +1 -0
  87. package/dist/scm/main.d.ts +3 -0
  88. package/dist/scm/main.js +15 -1
  89. package/dist/scm/readiness-cli.d.ts +2 -0
  90. package/dist/scm/readiness-cli.js +42 -1
  91. package/dist/scm/readiness.d.ts +2 -1
  92. package/dist/scm/readiness.js +7 -1
  93. package/dist/scope/decompose.js +2 -1
  94. package/dist/scope/delivery-evidence.d.ts +27 -1
  95. package/dist/scope/index.d.ts +1 -0
  96. package/dist/scope/index.js +1 -0
  97. package/dist/scope/lifecycle-write.d.ts +24 -0
  98. package/dist/scope/lifecycle-write.js +64 -0
  99. package/dist/scope/main.js +18 -6
  100. package/dist/scope/promote-from-issue.js +5 -5
  101. package/dist/scope/transition.js +90 -11
  102. package/dist/scope-provenance/evaluate.js +8 -4
  103. package/dist/session/ac-pass-banking.d.ts +5 -0
  104. package/dist/session/ac-pass-banking.js +3 -2
  105. package/dist/session/git.d.ts +18 -0
  106. package/dist/session/git.js +81 -1
  107. package/dist/session/occupancy.d.ts +2 -1
  108. package/dist/session/occupancy.js +11 -4
  109. package/dist/session/process-cost.d.ts +32 -10
  110. package/dist/session/process-cost.js +189 -1
  111. package/dist/session/product-state-hash.js +41 -24
  112. package/dist/session/ritual-entrypoint.js +16 -1
  113. package/dist/session/session-start.d.ts +18 -0
  114. package/dist/session/session-start.js +49 -2
  115. package/dist/session/verify-session-ritual.d.ts +46 -4
  116. package/dist/session/verify-session-ritual.js +118 -10
  117. package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
  118. package/dist/swarm/finalize-cohort-cli.js +165 -35
  119. package/dist/swarm/finalize-cohort.d.ts +1 -0
  120. package/dist/swarm/finalize-cohort.js +14 -2
  121. package/dist/swarm/launch.d.ts +69 -0
  122. package/dist/swarm/launch.js +191 -1
  123. package/dist/swarm/routing-set-cli.js +2 -0
  124. package/dist/swarm/routing-verify.d.ts +1 -1
  125. package/dist/swarm/routing-verify.js +2 -2
  126. package/dist/triage/actions/index.d.ts +4 -0
  127. package/dist/triage/actions/index.js +6 -0
  128. package/dist/triage/bootstrap/index.js +6 -6
  129. package/dist/triage/evaluate/evaluate.d.ts +11 -0
  130. package/dist/triage/evaluate/evaluate.js +169 -0
  131. package/dist/triage/evaluate/github.d.ts +16 -0
  132. package/dist/triage/evaluate/github.js +153 -0
  133. package/dist/triage/evaluate/index.d.ts +9 -0
  134. package/dist/triage/evaluate/index.js +8 -0
  135. package/dist/triage/evaluate/paths.d.ts +7 -0
  136. package/dist/triage/evaluate/paths.js +25 -0
  137. package/dist/triage/evaluate/sink.d.ts +5 -0
  138. package/dist/triage/evaluate/sink.js +52 -0
  139. package/dist/triage/evaluate/types.d.ts +93 -0
  140. package/dist/triage/evaluate/types.js +13 -0
  141. package/dist/triage/evaluate/validity.d.ts +8 -0
  142. package/dist/triage/evaluate/validity.js +87 -0
  143. package/dist/triage/evaluate/value.d.ts +12 -0
  144. package/dist/triage/evaluate/value.js +33 -0
  145. package/dist/triage/evaluate/wip-census.d.ts +5 -0
  146. package/dist/triage/evaluate/wip-census.js +39 -0
  147. package/dist/triage/evaluate/worktrees.d.ts +8 -0
  148. package/dist/triage/evaluate/worktrees.js +68 -0
  149. package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
  150. package/dist/triage/evaluate/xbrief-refs.js +60 -0
  151. package/dist/triage/help/registry-data.d.ts +22 -8
  152. package/dist/triage/help/registry-data.js +54 -6
  153. package/dist/triage/index.d.ts +1 -0
  154. package/dist/triage/index.js +1 -0
  155. package/dist/triage/welcome/writers.d.ts +1 -1
  156. package/dist/triage/welcome/writers.js +10 -5
  157. package/dist/value/feedback-file.d.ts +17 -2
  158. package/dist/value/feedback-file.js +80 -6
  159. package/dist/value/readback.d.ts +4 -1
  160. package/dist/value/readback.js +51 -12
  161. package/dist/vbrief-reconcile/labels.d.ts +1 -0
  162. package/dist/vbrief-reconcile/labels.js +30 -0
  163. package/dist/vbrief-validate/schema.d.ts +4 -0
  164. package/dist/vbrief-validate/schema.js +1 -1
  165. package/dist/vbrief-validation/story-quality.d.ts +7 -0
  166. package/dist/vbrief-validation/story-quality.js +8 -1
  167. package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
  168. package/dist/verify-env/agent-hook-readiness.js +12 -8
  169. package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
  170. package/dist/verify-env/agent-hooks-live-probe.js +26 -5
  171. package/dist/verify-env/agent-hooks.js +3 -4
  172. package/dist/xbrief-migrate/agents-header.js +69 -7
  173. package/package.json +7 -3
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) {
@@ -17,6 +17,7 @@ import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveL
17
17
  import { resolveRepo } from "../triage/queue/repo.js";
18
18
  import { referenceWithDefaultTrust, slugify } from "../vbrief-build/build.js";
19
19
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
20
+ import { READY_REQUIRES_PARALLEL_SAFE } from "../vbrief-validation/story-quality.js";
20
21
  import { formatCoverageReportLine, validateCoverageMap } from "./coverage-map.js";
21
22
  import { buildParentLineageArtifact } from "./parent-lineage.js";
22
23
  import { formatBriefJson } from "./vbrief-json.js";
@@ -407,7 +408,7 @@ export function storyQualityIssues(opts) {
407
408
  issues.push(...fileScopeIssues(swarm));
408
409
  issues.push(...verifyCommandIssues(swarm));
409
410
  if (swarm.parallel_safe === false) {
410
- issues.push("readiness=ready requires parallel_safe=true; use readiness=sequential or needs_refinement for non-concurrent work");
411
+ issues.push(READY_REQUIRES_PARALLEL_SAFE);
411
412
  }
412
413
  if (swarm.file_scope_confidence === "low") {
413
414
  issues.push("readiness=ready requires file_scope_confidence above low");
@@ -16,6 +16,29 @@ export declare const NON_DELIVERY_DISPOSITIONS: readonly ["cancelled", "supersed
16
16
  export type NonDeliveryDisposition = (typeof NON_DELIVERY_DISPOSITIONS)[number];
17
17
  /** Delivery verification outcome for completion provenance. */
18
18
  export type DeliveryDisposition = "delivered" | "merged_to_integration" | "not_delivered" | NonDeliveryDisposition | "unknown" | "unverified";
19
+ /**
20
+ * This is a historical audit snapshot stamped onto
21
+ * `plan.metadata.completionProvenance` at `scope:complete` (#3041 / #3690).
22
+ *
23
+ * Completed xBRIEFs have full standing as a record of *what is* and zero
24
+ * authority over *what to build next* (#3383). Fields stay because they
25
+ * answer a later reconstruction question after branches are deleted and
26
+ * refs move. A field does not need a current production-code reader.
27
+ *
28
+ * Reconstruction groups:
29
+ * - Identity and location: `repository`, `implementationCommit`, `prNumber`,
30
+ * `prBase`, `deliveryBranch`
31
+ * - Delivery evidence: `mergeCommit`, `deliveryCommit`, `disposition`,
32
+ * `handoffState`
33
+ * - Verification attribution: `verifiedAt`, `verifier`
34
+ * - Explicit operator-supplied facts: `deployed`, `uatVerified` (never
35
+ * inferred from Git)
36
+ *
37
+ * `completedSessionId` is duplicated. This nested copy is stamped so the
38
+ * snapshot is self-contained. Consumers (`verify:ac` /
39
+ * `session-completed-ac`) read the sibling
40
+ * `plan.metadata.completedSessionId`, not this field.
41
+ */
19
42
  export interface CompletionProvenance {
20
43
  readonly repository: string | null;
21
44
  readonly implementationCommit: string | null;
@@ -32,7 +55,10 @@ export interface CompletionProvenance {
32
55
  readonly deployed: boolean | null;
33
56
  /** Always null unless explicitly supplied — never inferred from Git (#3041). */
34
57
  readonly uatVerified: boolean | null;
35
- /** Session that completed the brief; used by check to target xbrief/completed (#3357). */
58
+ /**
59
+ * Nested session correlation for a self-contained snapshot.
60
+ * Consumers read sibling `plan.metadata.completedSessionId`, not this field.
61
+ */
36
62
  readonly completedSessionId?: string | null;
37
63
  }
38
64
  export interface DeliveryEvidenceInput {
@@ -8,6 +8,7 @@ export * from "./decomposed-refs.js";
8
8
  export * from "./delivery-evidence.js";
9
9
  export * from "./demote.js";
10
10
  export * from "./effort-activate-gate.js";
11
+ export * from "./lifecycle-write.js";
11
12
  export * from "./main.js";
12
13
  export * from "./open-umbrella-warning.js";
13
14
  export * from "./parent-lineage.js";
@@ -8,6 +8,7 @@ export * from "./decomposed-refs.js";
8
8
  export * from "./delivery-evidence.js";
9
9
  export * from "./demote.js";
10
10
  export * from "./effort-activate-gate.js";
11
+ export * from "./lifecycle-write.js";
11
12
  export * from "./main.js";
12
13
  export * from "./open-umbrella-warning.js";
13
14
  export * from "./parent-lineage.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Transition-write fingerprint for completed/ artifacts (#3679).
3
+ *
4
+ * Distinct from completionProvenance (#3686): this only records that
5
+ * runTransition wrote the blob. Non-code-bearing completes and scope:fail
6
+ * stamp this and still omit delivery provenance.
7
+ */
8
+ export declare const LIFECYCLE_WRITE_KEY: "lifecycleWrite";
9
+ export type LifecycleWriteAction = "complete" | "fail";
10
+ export interface LifecycleWriteStamp {
11
+ readonly action: LifecycleWriteAction;
12
+ readonly writtenAt: string;
13
+ }
14
+ /** Worker-facing leftover-land path after a scope-provenance strip (#3476 / #3679). */
15
+ export declare const LEFTOVER_LAND_PR_REMEDIATION: string;
16
+ /** Stamp that runTransition wrote this plan into completed/. */
17
+ export declare function stampLifecycleWrite(plan: Record<string, unknown>, action: LifecycleWriteAction, writtenAt: string): void;
18
+ /**
19
+ * True when a completed/ blob shows verb evidence.
20
+ * Accepts the new stamp, legacy completedAt (pre-#3679 complete), or failed status
21
+ * (scope:fail never stamped provenance and must keep passing).
22
+ */
23
+ export declare function hasTransitionWrite(plan: Record<string, unknown>): boolean;
24
+ //# sourceMappingURL=lifecycle-write.d.ts.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Transition-write fingerprint for completed/ artifacts (#3679).
3
+ *
4
+ * Distinct from completionProvenance (#3686): this only records that
5
+ * runTransition wrote the blob. Non-code-bearing completes and scope:fail
6
+ * stamp this and still omit delivery provenance.
7
+ */
8
+ export const LIFECYCLE_WRITE_KEY = "lifecycleWrite";
9
+ /** Worker-facing leftover-land path after a scope-provenance strip (#3476 / #3679). */
10
+ export const LEFTOVER_LAND_PR_REMEDIATION = "After a provenance-gated product PR strips active/ from the change set, " +
11
+ "the designed remainder is a leftover land PR (#3476): run `task scope:complete` " +
12
+ "after merge (it can stamp a brief already in completed/), then land that artifact. " +
13
+ "Do not git-add a completed/ husk to skip the verb.";
14
+ function asRecord(value) {
15
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
16
+ return null;
17
+ }
18
+ return value;
19
+ }
20
+ function metadataRecord(plan) {
21
+ const existing = asRecord(plan.metadata);
22
+ if (existing !== null) {
23
+ return existing;
24
+ }
25
+ const created = {};
26
+ plan.metadata = created;
27
+ return created;
28
+ }
29
+ /** Stamp that runTransition wrote this plan into completed/. */
30
+ export function stampLifecycleWrite(plan, action, writtenAt) {
31
+ const meta = metadataRecord(plan);
32
+ meta[LIFECYCLE_WRITE_KEY] = { action, writtenAt };
33
+ }
34
+ function hasLifecycleWriteStamp(plan) {
35
+ const meta = asRecord(plan.metadata);
36
+ if (meta === null) {
37
+ return false;
38
+ }
39
+ const stamp = asRecord(meta[LIFECYCLE_WRITE_KEY]);
40
+ if (stamp === null) {
41
+ return false;
42
+ }
43
+ const action = stamp.action;
44
+ const writtenAt = stamp.writtenAt;
45
+ return ((action === "complete" || action === "fail") &&
46
+ typeof writtenAt === "string" &&
47
+ writtenAt.trim().length > 0);
48
+ }
49
+ /**
50
+ * True when a completed/ blob shows verb evidence.
51
+ * Accepts the new stamp, legacy completedAt (pre-#3679 complete), or failed status
52
+ * (scope:fail never stamped provenance and must keep passing).
53
+ */
54
+ export function hasTransitionWrite(plan) {
55
+ if (hasLifecycleWriteStamp(plan)) {
56
+ return true;
57
+ }
58
+ const meta = asRecord(plan.metadata);
59
+ if (meta !== null && typeof meta.completedAt === "string" && meta.completedAt.trim().length > 0) {
60
+ return true;
61
+ }
62
+ return String(plan.status ?? "") === "failed";
63
+ }
64
+ //# sourceMappingURL=lifecycle-write.js.map
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
4
+ import { interceptHelp } from "../triage/help/index.js";
4
5
  import { reconcileUmbrellas, renderUmbrellasReport } from "../vbrief-reconcile/umbrellas.js";
5
6
  import { canonicalLogPath, readAll } from "./audit-log.js";
6
7
  import { batchPromote } from "./batch-promote.js";
@@ -13,14 +14,15 @@ import { promoteFromIssue } from "./promote-from-issue.js";
13
14
  import { promotePath } from "./promote-path.js";
14
15
  import { runTransition } from "./transition.js";
15
16
  import { findByDecisionId, isAlreadyUndone, REVERSIBLE_ACTIONS, undoBatch, undoOne, } from "./undo.js";
16
- const LIFECYCLE_USAGE_STDERR = "usage: scope_lifecycle.py [-h] [--project-root PROJECT_ROOT] [--force] [--batch]\n" +
17
- " [--from-issue N] [--repo OWNER/NAME] [--strict] [--force-no-cache]\n" +
17
+ const LIFECYCLE_USAGE_STDERR = "usage: deft scope:<action> [file] [--batch] [--from-issue N] [--repo OWNER/NAME]\n" +
18
+ " [--force] [--project-root PATH] [--strict] [--force-no-cache]\n" +
18
19
  " [--path PATH]\n" +
19
- " {activate,block,cancel,complete,fail,promote,restore,unblock}\n" +
20
- " [file ...]\n" +
21
- "scope_lifecycle.py: error: the following arguments are required: action, file\n" +
20
+ " complete also accepts: --merge-commit SHA --pr N [--pr-base BRANCH] [--delivery-branch BRANCH]\n" +
21
+ " actions: activate, block, cancel, complete, fail, promote, restore, unblock\n" +
22
+ "The verb already encodes the action (e.g. deft scope:promote <file>). Do not pass the action again.\n" +
22
23
  "(promote --batch may omit file and promotes all proposed/ scopes; #3011)\n" +
23
- "(promote --from-issue=N may omit file; #1136)\n";
24
+ "(promote --from-issue=N may omit file; #1136)\n" +
25
+ "(complete --merge-commit / --pr are required for code-bearing delivery evidence; #3041 / #3721)\n";
24
26
  function parseLifecycleArgv(argv) {
25
27
  if (argv.length < 1) {
26
28
  return { args: null, error: "usage" };
@@ -50,6 +52,8 @@ function parseLifecycleArgv(argv) {
50
52
  if (arg === undefined) {
51
53
  return { args: null, error: "usage" };
52
54
  }
55
+ if (arg === "--")
56
+ continue;
53
57
  if (arg === "--force") {
54
58
  force = true;
55
59
  }
@@ -236,6 +240,14 @@ function parseLifecycleArgv(argv) {
236
240
  }
237
241
  /** Main entry for scope_lifecycle.py parity. */
238
242
  export function lifecycleMain(argv) {
243
+ if (argv.includes("--help") || argv.includes("-h")) {
244
+ const helpRc = interceptHelp("scope_lifecycle", argv);
245
+ if (helpRc !== null) {
246
+ return helpRc;
247
+ }
248
+ process.stdout.write(LIFECYCLE_USAGE_STDERR);
249
+ return 0;
250
+ }
239
251
  const parsed = parseLifecycleArgv(argv);
240
252
  if (parsed.args === null) {
241
253
  if (parsed.error === "usage") {
@@ -195,7 +195,7 @@ export function promoteFromIssue(options) {
195
195
  return {
196
196
  ok: false,
197
197
  message: `No triage-cache decision for #${n} (${repoSafe}). ` +
198
- `Accept first: task triage:accept -- --issue ${n} --repo ${repoSafe} ` +
198
+ `Accept first: deft triage:accept --issue ${n} --repo ${repoSafe} ` +
199
199
  `(or omit --strict to soft-warn and proceed; --force-no-cache skips the gate).`,
200
200
  exitCode: 1,
201
201
  warnings,
@@ -212,7 +212,7 @@ export function promoteFromIssue(options) {
212
212
  ok: false,
213
213
  message: `Refusing promote for #${n} (${repoSafe}): latest triage decision is '${sanitizeMsg(latest.decision)}' ` +
214
214
  `(decision_id=${latest.decision_id}). ` +
215
- `Accept first: task triage:accept -- --issue ${n} --repo ${repoSafe} ` +
215
+ `Accept first: deft triage:accept --issue ${n} --repo ${repoSafe} ` +
216
216
  `or override with --force-no-cache.`,
217
217
  exitCode: 1,
218
218
  warnings,
@@ -315,8 +315,8 @@ export function promoteFromIssue(options) {
315
315
  return {
316
316
  ok: false,
317
317
  message: `No proposed/ scope artifact found for issue #${n} (${repoSafe}). ` +
318
- `Ingest first (task triage:accept -- --issue ${n} --repo ${repoSafe}) ` +
319
- `or pass an explicit path: task scope:promote -- <path>.`,
318
+ `Ingest first (deft triage:accept --issue ${n} --repo ${repoSafe}) ` +
319
+ `or pass an explicit path: deft scope:promote <path>.`,
320
320
  exitCode: 1,
321
321
  warnings,
322
322
  repo,
@@ -330,7 +330,7 @@ export function promoteFromIssue(options) {
330
330
  return {
331
331
  ok: false,
332
332
  message: `Multiple proposed/ artifacts match issue #${n}; refuse to guess.\n${list}\n` +
333
- `Promote one path explicitly: task scope:promote -- <path> ` +
333
+ `Promote one path explicitly: deft scope:promote <path> ` +
334
334
  `or re-run with --path <one-of-the-above>.`,
335
335
  exitCode: 1,
336
336
  warnings,
@@ -16,6 +16,7 @@ import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRAN
16
16
  import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
17
17
  import { classifyStoredDeliveryDisposition, evaluateDeliveryGate, resolveCompletionSessionId, stampDeliveryProvenance, } from "./delivery-evidence.js";
18
18
  import { evaluateEffortActivateGate } from "./effort-activate-gate.js";
19
+ import { stampLifecycleWrite } from "./lifecycle-write.js";
19
20
  import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
20
21
  import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
21
22
  import { utcNowIso } from "./vbrief-json.js";
@@ -81,7 +82,9 @@ export function runTransition(action, filePath, now = new Date(), options = {})
81
82
  };
82
83
  }
83
84
  const { allowedSources, targetFolder, targetStatus } = TRANSITIONS[act];
84
- if (!allowedSources.includes(currentFolder)) {
85
+ const restampCompleted = act === "complete" && currentFolder === "completed";
86
+ if (!restampCompleted &&
87
+ !allowedSources.includes(currentFolder)) {
85
88
  const allowedStr = allowedSources.map((s) => `${s}/`).join(", ");
86
89
  return {
87
90
  ok: false,
@@ -103,15 +106,9 @@ export function runTransition(action, filePath, now = new Date(), options = {})
103
106
  const requiredStatus = STATUS_PRECONDITIONS[act];
104
107
  if (requiredStatus !== undefined) {
105
108
  if (currentStatus === targetStatus) {
106
- // Surface legacy delivery disposition on already-completed briefs (#3041).
107
- let dispositionSuffix = "";
108
- if (act === "complete" && currentFolder === "completed") {
109
- const disposition = classifyStoredDeliveryDisposition(planObj);
110
- dispositionSuffix = ` (deliveryDisposition=${disposition})`;
111
- }
112
109
  return {
113
110
  ok: true,
114
- message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/${dispositionSuffix}`,
111
+ message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/`,
115
112
  };
116
113
  }
117
114
  if (currentStatus !== requiredStatus) {
@@ -122,14 +119,30 @@ export function runTransition(action, filePath, now = new Date(), options = {})
122
119
  };
123
120
  }
124
121
  }
122
+ const nowIso = utcNowIso(now);
123
+ const vbriefRoot = dirname(dirname(resolvedPath));
124
+ const projectRoot = dirname(vbriefRoot);
125
+ // #3679: lift #3041 already-completed handling out of STATUS_PRECONDITIONS
126
+ // so complete can stamp a brief already in completed/ without a folder trip.
127
+ if (restampCompleted) {
128
+ return restampCompletedBrief({
129
+ resolvedPath,
130
+ basename,
131
+ data,
132
+ planObj,
133
+ projectRoot,
134
+ vbriefRoot,
135
+ now,
136
+ nowIso,
137
+ options,
138
+ });
139
+ }
125
140
  if (targetFolder !== null && targetFolder === currentFolder) {
126
141
  return {
127
142
  ok: true,
128
143
  message: `No-op: ${basename} is already in ${currentFolder}/ (status: ${currentStatus})`,
129
144
  };
130
145
  }
131
- const vbriefRoot = dirname(dirname(resolvedPath));
132
- const projectRoot = dirname(vbriefRoot);
133
146
  if (targetFolder !== null) {
134
147
  const destDir = join(vbriefRoot, targetFolder);
135
148
  try {
@@ -144,7 +157,6 @@ export function runTransition(action, filePath, now = new Date(), options = {})
144
157
  throw err;
145
158
  }
146
159
  }
147
- const nowIso = utcNowIso(now);
148
160
  // #3360: hand-authored briefs run #3323 clause derivation on activate/promote.
149
161
  // #3355: stamp is state-observed (before vs after), not derivation-notice-bound.
150
162
  const previousAcceptance = planObj.acceptance;
@@ -190,6 +202,7 @@ export function runTransition(action, filePath, now = new Date(), options = {})
190
202
  ? { ...gate.provenance, completedSessionId: sessionId }
191
203
  : gate.provenance);
192
204
  }
205
+ stampLifecycleWrite(planObj, "complete", nowIso);
193
206
  }
194
207
  // #3240: per-criterion typed evidence or human-origin disposition before auto-advance.
195
208
  let acceptanceReports;
@@ -266,6 +279,9 @@ export function runTransition(action, filePath, now = new Date(), options = {})
266
279
  };
267
280
  }
268
281
  }
282
+ if (act === "fail") {
283
+ stampLifecycleWrite(planObj, "fail", nowIso);
284
+ }
269
285
  const formatted = formatBriefJson(data);
270
286
  const crud = new InstrumentedVbriefCrud({ now: () => now });
271
287
  if (targetFolder !== null) {
@@ -370,6 +386,69 @@ export function runTransition(action, filePath, now = new Date(), options = {})
370
386
  acceptanceReports,
371
387
  };
372
388
  }
389
+ /**
390
+ * Stamp a brief already in completed/ (#3679). Runs the #3041 delivery gate
391
+ * and writes in place so recovery does not need a folder round-trip.
392
+ * The #3041 deliveryDisposition suffix now actually executes for complete.
393
+ */
394
+ function restampCompletedBrief(args) {
395
+ const { resolvedPath, basename, data, planObj, projectRoot, vbriefRoot, now, nowIso, options } = args;
396
+ const gate = evaluateDeliveryGate({
397
+ projectRoot,
398
+ plan: planObj,
399
+ nowIso,
400
+ evidence: options.deliveryEvidence,
401
+ nonDeliveryDisposition: options.nonDeliveryDisposition,
402
+ runGit: options.runGit,
403
+ verifier: options.verifier ?? "scope:complete",
404
+ assumeEvidenceValidated: options.assumeEvidenceValidated,
405
+ });
406
+ if (!gate.ok) {
407
+ return { ok: false, message: gate.message };
408
+ }
409
+ if (gate.provenance !== null) {
410
+ const sessionId = resolveCompletionSessionId(projectRoot);
411
+ stampDeliveryProvenance(planObj, sessionId !== null ? { ...gate.provenance, completedSessionId: sessionId } : gate.provenance);
412
+ }
413
+ planObj.status = "completed";
414
+ planObj.updated = nowIso;
415
+ stampEnvelopeUpdated(data, nowIso);
416
+ stampCompletionMetadata(planObj, projectRoot, nowIso, {
417
+ completedSessionId: resolveCompletionSessionId(projectRoot),
418
+ });
419
+ stampLifecycleWrite(planObj, "complete", nowIso);
420
+ const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot, { projectRoot });
421
+ if (!writeResult.ok) {
422
+ return { ok: false, message: writeResult.message };
423
+ }
424
+ const crud = new InstrumentedVbriefCrud({ now: () => now });
425
+ crud.recordTrustedUpdate(resolvedPath, formatBriefJson(data));
426
+ try {
427
+ persistCrudMetrics(projectRoot, crud.getMetrics());
428
+ }
429
+ catch {
430
+ /* best-effort telemetry persistence */
431
+ }
432
+ const sessionId = resolveCompletionSessionId(projectRoot);
433
+ if (sessionId !== null) {
434
+ try {
435
+ writeSessionCompletedMarker(projectRoot, {
436
+ path: resolvedPath,
437
+ sessionId,
438
+ completedAt: nowIso,
439
+ });
440
+ }
441
+ catch {
442
+ return { ok: false, message: SESSION_COMPLETED_AC_REMEDIATION };
443
+ }
444
+ }
445
+ const disposition = classifyStoredDeliveryDisposition(planObj);
446
+ return {
447
+ ok: true,
448
+ message: `Restamped ${basename} in completed/ (status: completed, ` +
449
+ `deliveryDisposition=${disposition})`,
450
+ };
451
+ }
373
452
  export function recordWipCapOverride(filePath, projectRoot, check, now = new Date()) {
374
453
  try {
375
454
  const rel = resolve(filePath).startsWith(resolve(projectRoot))
@@ -205,11 +205,15 @@ function listActiveXbriefPaths(projectRoot) {
205
205
  }
206
206
  function remediationForExpansion() {
207
207
  return ("Renew human approval: re-record the approved-scope digest after operator review " +
208
- "(`task scope:record-approved-scope -- <xbrief-path> --actor <you>` writes " +
208
+ "(`task scope:record-approved-scope -- <xbrief-path> --actor <you> --confirm` writes " +
209
209
  "`.deft/approved-scope/<plan-id>.json` with a humanApproval stamp). Commit that " +
210
210
  "approval on the merge base (or a prior PR) before expanding or activating the " +
211
211
  "scoped xBRIEF in the implementation change set. Editing the active xBRIEF alone " +
212
- "does not authorize new paths (#3145 / #3205). See content/docs/scope-provenance.md.");
212
+ "does not authorize new paths (#3145 / #3205). See content/docs/scope-provenance.md. " +
213
+ "If you cannot mint a digest (human TTY required), drop active/ from this product " +
214
+ "change set and land the completed artifact in a leftover land PR after merge (#3476). " +
215
+ "Run `task scope:complete` on the brief (it can stamp a file already in completed/). " +
216
+ "Do not git-add a completed/ husk to skip that PR (#3679).");
213
217
  }
214
218
  /**
215
219
  * Parse + lightly validate an approved-scope JSON blob (base-ref `git show` or disk).
@@ -344,7 +348,7 @@ export function evaluateOneScopeProvenance(input) {
344
348
  detail: "active xBRIEF modified with a non-human (agent/missing) approved-scope stamp; " +
345
349
  "only humanApproval stamps authorize non-empty file_scope",
346
350
  remediation: "Record a human-origin approval via `task scope:record-approved-scope -- " +
347
- "<xbrief-path> --actor <you>` (#3145 / #3205).",
351
+ "<xbrief-path> --actor <you> --confirm` (#3145 / #3205).",
348
352
  };
349
353
  }
350
354
  }
@@ -361,7 +365,7 @@ export function evaluateOneScopeProvenance(input) {
361
365
  detail: "active xBRIEF modified with a non-human approved-scope stamp (scope shrink/noise path); " +
362
366
  "only humanApproval stamps authorize non-empty file_scope",
363
367
  remediation: "Record a human-origin approval via `task scope:record-approved-scope -- " +
364
- "<xbrief-path> --actor <you>` (#3145 / #3205).",
368
+ "<xbrief-path> --actor <you> --confirm` (#3145 / #3205).",
365
369
  };
366
370
  }
367
371
  return null;
@@ -173,6 +173,11 @@ export interface MaybeBankOnAcPassInput {
173
173
  readonly executableRuns: number;
174
174
  readonly quiet?: boolean;
175
175
  readonly productStateHash?: string | null;
176
+ /**
177
+ * Bank a verified-pass walk even when no shell runs were recorded (#3558).
178
+ * Soft/advisory empty passes still skip via executableRuns === 0.
179
+ */
180
+ readonly verifiedPass?: boolean;
176
181
  }
177
182
  export interface MaybeBankOnAcPassResult {
178
183
  readonly banked: boolean;