@deftai/directive-core 0.89.0 → 0.91.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/doctor/constants.d.ts +1 -1
  2. package/dist/doctor/constants.js +2 -0
  3. package/dist/doctor/flags.js +21 -1
  4. package/dist/doctor/index.d.ts +1 -0
  5. package/dist/doctor/index.js +1 -0
  6. package/dist/doctor/main.js +22 -0
  7. package/dist/doctor/openclaw-skills.d.ts +143 -0
  8. package/dist/doctor/openclaw-skills.js +706 -0
  9. package/dist/doctor/types.d.ts +11 -0
  10. package/dist/eval/health.d.ts +10 -1
  11. package/dist/eval/health.js +16 -16
  12. package/dist/hooks/dispatcher.js +20 -5
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +1 -0
  15. package/dist/init-deposit/hygiene.js +3 -0
  16. package/dist/init-deposit/init-deposit.d.ts +5 -0
  17. package/dist/init-deposit/init-deposit.js +37 -2
  18. package/dist/intake/github-auth-modes.d.ts +1 -1
  19. package/dist/intake/github-auth-modes.js +25 -3
  20. package/dist/intake/issue-ingest-cli.js +13 -0
  21. package/dist/intake/reconcile-issues-cli.js +13 -0
  22. package/dist/lifecycle/events.js +2 -0
  23. package/dist/lifecycle/index.d.ts +1 -0
  24. package/dist/lifecycle/index.js +1 -0
  25. package/dist/lifecycle/stats.d.ts +53 -0
  26. package/dist/lifecycle/stats.js +286 -0
  27. package/dist/policy/plan-extensions.d.ts +10 -0
  28. package/dist/policy/plan-extensions.js +16 -0
  29. package/dist/pr-closing-keywords/detect.d.ts +13 -0
  30. package/dist/pr-closing-keywords/detect.js +73 -8
  31. package/dist/pr-closing-keywords/index.d.ts +2 -2
  32. package/dist/pr-closing-keywords/index.js +1 -1
  33. package/dist/pr-closing-keywords/main.js +119 -90
  34. package/dist/pr-closing-keywords/types.d.ts +10 -0
  35. package/dist/release/spawn.js +13 -0
  36. package/dist/release-e2e/git-ops.d.ts +7 -0
  37. package/dist/release-e2e/git-ops.js +35 -2
  38. package/dist/render/constants.d.ts +16 -0
  39. package/dist/render/constants.js +16 -1
  40. package/dist/render/export-spec.d.ts +7 -1
  41. package/dist/render/export-spec.js +51 -5
  42. package/dist/render/index.d.ts +3 -2
  43. package/dist/render/index.js +2 -2
  44. package/dist/render/roadmap-render.js +164 -60
  45. package/dist/render/scope-outlook.d.ts +5 -0
  46. package/dist/render/scope-outlook.js +3 -0
  47. package/dist/render/spec-render.d.ts +28 -2
  48. package/dist/render/spec-render.js +111 -14
  49. package/dist/render/text-utils.d.ts +6 -0
  50. package/dist/render/text-utils.js +23 -0
  51. package/dist/scm/binary.d.ts +3 -0
  52. package/dist/scm/binary.js +10 -2
  53. package/dist/scm/call.d.ts +5 -0
  54. package/dist/scm/call.js +5 -0
  55. package/dist/scm/index.d.ts +1 -0
  56. package/dist/scm/index.js +1 -0
  57. package/dist/scm/main.d.ts +5 -0
  58. package/dist/scm/main.js +27 -0
  59. package/dist/scm/readiness-cli.d.ts +24 -0
  60. package/dist/scm/readiness-cli.js +85 -0
  61. package/dist/scm/readiness.d.ts +104 -0
  62. package/dist/scm/readiness.js +421 -0
  63. package/dist/scope/batch-promote.d.ts +26 -0
  64. package/dist/scope/batch-promote.js +167 -0
  65. package/dist/scope/index.d.ts +1 -0
  66. package/dist/scope/index.js +1 -0
  67. package/dist/scope/main.d.ts +2 -0
  68. package/dist/scope/main.js +58 -7
  69. package/dist/scope/wip-cap-check.d.ts +6 -0
  70. package/dist/scope/wip-cap-check.js +14 -2
  71. package/dist/session/index.d.ts +2 -0
  72. package/dist/session/index.js +2 -0
  73. package/dist/session/process-cost-constants.d.ts +9 -0
  74. package/dist/session/process-cost-constants.js +11 -0
  75. package/dist/session/process-cost.d.ts +53 -0
  76. package/dist/session/process-cost.js +82 -0
  77. package/dist/session/ritual-sentinel.d.ts +5 -0
  78. package/dist/session/ritual-sentinel.js +12 -1
  79. package/dist/session/session-ready.d.ts +67 -0
  80. package/dist/session/session-ready.js +264 -0
  81. package/dist/session/session-start.d.ts +63 -1
  82. package/dist/session/session-start.js +432 -24
  83. package/dist/session/verify-session-ritual.d.ts +8 -0
  84. package/dist/session/verify-session-ritual.js +95 -35
  85. package/dist/tool-events/classify.d.ts +20 -0
  86. package/dist/tool-events/classify.js +634 -0
  87. package/dist/tool-events/index.d.ts +10 -0
  88. package/dist/tool-events/index.js +10 -0
  89. package/dist/tool-events/summarize.d.ts +34 -0
  90. package/dist/tool-events/summarize.js +93 -0
  91. package/dist/tool-events/types.d.ts +52 -0
  92. package/dist/tool-events/types.js +13 -0
  93. package/dist/triage/welcome/index.d.ts +1 -1
  94. package/dist/triage/welcome/index.js +1 -1
  95. package/dist/triage/welcome/onboard.js +7 -1
  96. package/dist/triage/welcome/prior-state.d.ts +7 -0
  97. package/dist/triage/welcome/prior-state.js +16 -2
  98. package/dist/triage/welcome/writers.d.ts +10 -0
  99. package/dist/triage/welcome/writers.js +72 -2
  100. package/package.json +7 -3
@@ -75,6 +75,10 @@ export interface DoctorFlags {
75
75
  readonly network: boolean;
76
76
  readonly help: boolean;
77
77
  readonly projectRoot: string | null;
78
+ /** Replace divergent OpenClaw pin dirs during doctor --fix (#3001). */
79
+ readonly force: boolean;
80
+ /** Wire always-pins into main + every workspace-* seat (#3001). */
81
+ readonly openclawAllAgents: boolean;
78
82
  readonly unknown: readonly string[];
79
83
  }
80
84
  export interface ThrottleDecision {
@@ -161,5 +165,12 @@ export interface DoctorSeams {
161
165
  readonly distance: "current" | "behind-minor" | "behind-major";
162
166
  readonly stale: boolean;
163
167
  };
168
+ /**
169
+ * OpenClaw skill-pin seams (#3001). Injected so detect/fix stays offline +
170
+ * deterministic in tests (fake HOME / env / fs).
171
+ */
172
+ readonly openclawEnv?: NodeJS.ProcessEnv;
173
+ readonly openclawHomeDir?: () => string;
174
+ readonly openclawContentRootFor?: (frameworkRoot: string) => string;
164
175
  }
165
176
  //# sourceMappingURL=types.d.ts.map
@@ -40,7 +40,16 @@ export interface EvaluateHealthResult {
40
40
  }
41
41
  /** Absolute path to the versioned health history ledger (#2545). */
42
42
  export declare function healthHistoryPath(projectRoot: string): string | null;
43
- /** Detect the canonical wipCap unsatisfiable-nudge contradiction (#1694). */
43
+ /**
44
+ * Residual regression detector for the #1694 wipCap unsatisfiable-nudge.
45
+ *
46
+ * Pre-fix: classify treated absent `plan.policy.wipCap` as incomplete while
47
+ * omit-by-design (#1186 D1) made materializing the default a self-check trap.
48
+ * The fix records decision-provenance out-of-band (`x-directive/onboarding`),
49
+ * so greenfield "missing wipCap decision" is satisfiable without materializing
50
+ * the value field. This detector now only fires if a decision marker is set
51
+ * but classify still lists wipCap as missing (classifier regression).
52
+ */
44
53
  export declare function detectWipCapUnsatisfiableNudge(projectRoot: string): ContradictionEvidence | null;
45
54
  /** Run all registered contradictory-gate detectors. */
46
55
  export declare function detectContradictoryGates(projectRoot: string): ContradictionEvidence[];
@@ -6,7 +6,6 @@ import { readCorePackageVersion } from "../engine-version.js";
6
6
  import { containedWrite } from "../fs/contained-write.js";
7
7
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
8
8
  import { healthMetricsHistoryPath } from "../metrics/resolve-metrics-home.js";
9
- import { readPlanPolicy } from "../policy/plan-extensions.js";
10
9
  import { classifyOnboarding, detectPriorState } from "../triage/welcome/prior-state.js";
11
10
  import { validateLinks } from "../validate-content/index.js";
12
11
  import { evaluateConformance } from "../vbrief-validate/conformance.js";
@@ -106,26 +105,27 @@ function probeContentManifest(projectRoot) {
106
105
  export function healthHistoryPath(projectRoot) {
107
106
  return healthMetricsHistoryPath(projectRoot);
108
107
  }
109
- /** Detect the canonical wipCap unsatisfiable-nudge contradiction (#1694). */
108
+ /**
109
+ * Residual regression detector for the #1694 wipCap unsatisfiable-nudge.
110
+ *
111
+ * Pre-fix: classify treated absent `plan.policy.wipCap` as incomplete while
112
+ * omit-by-design (#1186 D1) made materializing the default a self-check trap.
113
+ * The fix records decision-provenance out-of-band (`x-directive/onboarding`),
114
+ * so greenfield "missing wipCap decision" is satisfiable without materializing
115
+ * the value field. This detector now only fires if a decision marker is set
116
+ * but classify still lists wipCap as missing (classifier regression).
117
+ */
110
118
  export function detectWipCapUnsatisfiableNudge(projectRoot) {
111
119
  const loaded = loadPlan(projectRoot);
112
120
  if (loaded === null) {
113
121
  return null;
114
122
  }
115
- const { plan, filepath } = loaded;
116
- const policy = readPlanPolicy(plan);
117
- const wipCapPresent = typeof policy === "object" &&
118
- policy !== null &&
119
- !Array.isArray(policy) &&
120
- "wipCap" in policy;
121
- if (wipCapPresent) {
122
- return null;
123
- }
124
123
  const state = detectPriorState(projectRoot);
125
124
  const [, missing] = classifyOnboarding(state);
126
- if (!missing.includes("wipCap")) {
125
+ if (!(state.wipCapDecided && missing.includes("wipCap"))) {
127
126
  return null;
128
127
  }
128
+ const { plan, filepath } = loaded;
129
129
  const validatorErrors = validateWipCapOnPlan(plan, filepath);
130
130
  if (validatorErrors.length > 0) {
131
131
  return null;
@@ -133,11 +133,11 @@ export function detectWipCapUnsatisfiableNudge(projectRoot) {
133
133
  return {
134
134
  id: "wipCap-unsatisfiable-nudge",
135
135
  kind: "unsatisfiable-nudge",
136
- summary: "Onboarding completeness treats absent plan.policy.wipCap as incomplete, but omit-by-design accepts absence as valid (#1694 / #1186 D1).",
136
+ summary: "wipCap decision marker is set but classifyOnboarding still lists wipCap as missing (#1694 regression).",
137
137
  signals: [
138
- "classifyOnboarding: wipCap listed in missing onboarding signals",
139
- "validateWipCapOnPlan: omitted wipCap is valid",
140
- "triage:welcome --onboard nudge cannot clear without violating omit-by-design contract",
138
+ "x-directive/onboarding.wipCapDecided: true",
139
+ "classifyOnboarding: wipCap still in missing onboarding signals",
140
+ "decision-provenance and completeness classifier are out of sync",
141
141
  ],
142
142
  };
143
143
  }
@@ -7,9 +7,10 @@ import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRE
7
7
  import { classifyMcpTool, DEFAULT_RUNTIME_AUTHORITY_POLICY, evaluateRuntimeAuthorityDirectWrite, evaluateRuntimeAuthorityShellOp, listShellOps, loadRuntimeAuthorityFromProject, } from "../policy/runtime-authority.js";
8
8
  import { loadStoryWriteFenceFromPath, resolveWriteFence } from "../policy/write-fence.js";
9
9
  import { detectBranch } from "../session/git.js";
10
+ import { emitSessionRitualBlockedProcessCost } from "../session/process-cost.js";
10
11
  import { markRitualStaleAfterCompact } from "../session/ritual-sentinel.js";
11
12
  import { runSessionStartHookWrite } from "../session/session-start-hook.js";
12
- import { inspectSessionRitual } from "../session/verify-session-ritual.js";
13
+ import { formatRitualRecoveryInstruction, inspectSessionRitual, } from "../session/verify-session-ritual.js";
13
14
  import { hookMcpArgsText, hookShellCommand, hookToolName, hookWriteTargetPath, missingToolNameMessage, record, } from "./classify/index.js";
14
15
  import { isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
15
16
  import { inspectActiveScope } from "./scope.js";
@@ -452,13 +453,27 @@ function inspectMutationGates(input, toolName, seams, options) {
452
453
  ((root) => inspectSessionRitual(root, { tier: "gated", posture: "mutation" })))(projectRoot);
453
454
  }
454
455
  catch (cause) {
456
+ // #2994: best-effort local process-cost; never changes deny verdict.
457
+ emitSessionRitualBlockedProcessCost({
458
+ toolName,
459
+ code: "ritual-not-ready",
460
+ recoveryTier: "cold",
461
+ detail: `inspect threw: ${String(cause)}`,
462
+ }, { projectRoot });
455
463
  return deny(input, "ritual-not-ready", toolName, `Directive could not inspect the gated session ritual: ${String(cause)}. ` +
456
- "Run `deft session:start`, then `deft verify:session-ritual -- --tier=gated`.");
464
+ formatRitualRecoveryInstruction("cold"));
457
465
  }
458
466
  if (ritual.code !== 0) {
459
- return deny(input, "ritual-not-ready", toolName, `Directive denied ${toolName}: ${ritual.message} ` +
460
- "Recovery: run `deft session:start`, then " +
461
- "`deft verify:session-ritual -- --tier=gated`.");
467
+ // #2992: prefer re-arm recovery when age/compact stale; cold when bind invalid.
468
+ const recoveryTier = ritual.recoveryTier === "rearm" ? "rearm" : "cold";
469
+ // #2994: best-effort local process-cost; never changes deny verdict.
470
+ emitSessionRitualBlockedProcessCost({
471
+ toolName,
472
+ code: "ritual-not-ready",
473
+ recoveryTier,
474
+ detail: ritual.message,
475
+ }, { projectRoot });
476
+ return deny(input, "ritual-not-ready", toolName, `Directive denied ${toolName}: ${ritual.message} ${formatRitualRecoveryInstruction(recoveryTier)}`);
462
477
  }
463
478
  if (options.proposedLifecycleExempt) {
464
479
  const writeTarget = hookWriteTargetPath(input.payload);
package/dist/index.d.ts CHANGED
@@ -53,6 +53,7 @@ export * as session from "./session/index.js";
53
53
  export * as slice from "./slice/index.js";
54
54
  export * as storyReady from "./story-ready/index.js";
55
55
  export * as swarm from "./swarm/index.js";
56
+ export * as toolEvents from "./tool-events/index.js";
56
57
  export * as triage from "./triage/index.js";
57
58
  export * as userConfig from "./user-config/index.js";
58
59
  export * as validateContent from "./validate-content/index.js";
package/dist/index.js CHANGED
@@ -54,6 +54,7 @@ export * as session from "./session/index.js";
54
54
  export * as slice from "./slice/index.js";
55
55
  export * as storyReady from "./story-ready/index.js";
56
56
  export * as swarm from "./swarm/index.js";
57
+ export * as toolEvents from "./tool-events/index.js";
57
58
  export * as triage from "./triage/index.js";
58
59
  export * as userConfig from "./user-config/index.js";
59
60
  export * as validateContent from "./validate-content/index.js";
@@ -45,6 +45,9 @@ export function installerManagedMatchers() {
45
45
  // `deft update` framework-deposit PR trips no-mixed-core-and-app (#2277).
46
46
  { exact: "xbrief/.deft-version" },
47
47
  { exact: "xbrief/xbrief.md" },
48
+ // Minimal render-ready seed from init (#3013); operator may later edit identity.
49
+ { exact: "xbrief/PROJECT-DEFINITION.xbrief.json" },
50
+ { exact: "vbrief/PROJECT-DEFINITION.vbrief.json" },
48
51
  { prefix: "xbrief/schemas/" },
49
52
  { prefix: "xbrief/migration/" },
50
53
  ...VBRIEF_LIFECYCLE_DIRS.map((sub) => ({ exact: `xbrief/${sub}/.gitkeep` })),
@@ -34,6 +34,11 @@ export declare function userConfigDir(): string;
34
34
  export declare function createUserConfigDir(io: InitDepositIo): string;
35
35
  export declare function buildInstallSummaryJson(result: InitDepositResult, options: InitDepositArgs): Record<string, unknown>;
36
36
  export declare function printNextSteps(result: InitDepositResult, io: InitDepositIo): void;
37
+ /**
38
+ * Ensure a minimal render-ready PROJECT-DEFINITION exists after lifecycle dirs
39
+ * are deposited (#3013 / epic #3009). Idempotent: never overwrites existing identity.
40
+ */
41
+ export declare function seedMinimalProjectDefinition(projectDir: string, io: InitDepositIo): boolean;
37
42
  export declare function runInitDeposit(args: InitDepositArgs, io: InitDepositIo, seams?: InitDepositSeams): Promise<InitDepositResult>;
38
43
  export interface RunInitDepositCliOptions extends InitDepositArgs {
39
44
  readonly writeOut: (text: string) => void;
@@ -7,12 +7,14 @@
7
7
  */
8
8
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
9
9
  import { homedir, platform } from "node:os";
10
- import { join, resolve } from "node:path";
10
+ import { basename, join, resolve } from "node:path";
11
11
  import { assertDepositContained } from "../deposit/contain.js";
12
12
  import { copyTree } from "../deposit/copy-tree.js";
13
13
  import { prunePythonArtifactsFromDeposit } from "../deposit/python-free.js";
14
14
  import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
15
15
  import { readCorePackageVersion } from "../engine-version.js";
16
+ import { renderProjectDefinition } from "../render/project-render.js";
17
+ import { removeStaleMigratedFrameworkNarrative } from "../xbrief-migrate/migrate-project.js";
16
18
  import { writeAgentHookDeposit } from "./agent-hooks.js";
17
19
  import { ensureInitGitignoreLines, reconstituteDepositFromContent } from "./gitignore.js";
18
20
  import { depositStagePaths, printCommitGuidance } from "./hygiene.js";
@@ -20,6 +22,7 @@ import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout,
20
22
  import { printMigrateNudgeIfNeeded } from "./migrate.js";
21
23
  import { ensurePrettierIgnoreLines } from "./prettierignore.js";
22
24
  import { CANONICAL_INSTALL_ROOT, depositNeutralization, ensureTaskfile, writeAgentsMd, writeAgentsSkills, writeConsumerGitHooks, writeConsumerVbrief, writeInstallManifest, } from "./scaffold.js";
25
+ import { syncBareVersionMarker } from "./xbrief-projections.js";
23
26
  export function parseInitArgv(canonicalArgv, userArgv = []) {
24
27
  const args = [...canonicalArgv, ...userArgv];
25
28
  let projectDir = process.cwd();
@@ -113,10 +116,36 @@ export function printNextSteps(result, io) {
113
116
  io.printf(` 1. Open your AI coding assistant in ${result.projectDir}\n`);
114
117
  io.printf(" 2. Deft skill auto-discovery is partially implemented — if your agent doesn't\n");
115
118
  io.printf(' start setup automatically, tell it: "Use AGENTS.md"\n');
116
- io.printf(" 3. On first session, the agent will guide you through creating USER.md and PROJECT-DEFINITION.vbrief.json\n");
119
+ io.printf(" 3. On first session, the agent will guide you through USER.md (if missing).\n");
120
+ io.printf(" 4. PROJECT-DEFINITION is seeded at init (#3013) — run `task project:render` once to refresh items from lifecycle folders; do not multi-turn invent project identity.\n");
117
121
  printMigrateNudgeIfNeeded(result.projectDir, io);
118
122
  io.printf("\n");
119
123
  }
124
+ /**
125
+ * Ensure a minimal render-ready PROJECT-DEFINITION exists after lifecycle dirs
126
+ * are deposited (#3013 / epic #3009). Idempotent: never overwrites existing identity.
127
+ */
128
+ export function seedMinimalProjectDefinition(projectDir, io) {
129
+ const xbriefDir = join(projectDir, "xbrief");
130
+ const legacyDir = join(projectDir, "vbrief");
131
+ const root = existsSync(xbriefDir) ? xbriefDir : existsSync(legacyDir) ? legacyDir : null;
132
+ if (root === null) {
133
+ return false;
134
+ }
135
+ const isXbrief = basename(root) === "xbrief";
136
+ const projectDefPath = join(root, isXbrief ? "PROJECT-DEFINITION.xbrief.json" : "PROJECT-DEFINITION.vbrief.json");
137
+ if (existsSync(projectDefPath)) {
138
+ io.printf("PROJECT-DEFINITION already present — leaving identity intact (#3013).\n");
139
+ return false;
140
+ }
141
+ const [ok, message] = renderProjectDefinition(root);
142
+ if (ok) {
143
+ io.printf(`${message} (minimal seed for one-shot project:render; #3013)\n`);
144
+ return true;
145
+ }
146
+ io.printf(`PROJECT-DEFINITION seed skipped: ${message}\n`);
147
+ return false;
148
+ }
120
149
  export async function runInitDeposit(args, io, seams = {}) {
121
150
  const projectDir = args.projectDir;
122
151
  const deftDir = join(projectDir, CANONICAL_INSTALL_ROOT);
@@ -154,6 +183,12 @@ export async function runInitDeposit(args, io, seams = {}) {
154
183
  const skillsCreated = writeAgentsSkills(projectDir, io);
155
184
  await depositNeutralization(projectDir, io);
156
185
  await writeConsumerVbrief(projectDir, deftDir, io);
186
+ seedMinimalProjectDefinition(projectDir, io);
187
+ // Seeding PROJECT-DEFINITION.xbrief.json makes resolveLifecycleRoot succeed.
188
+ // Align the same consumer derivatives update repairs (#2595 / #2806) so
189
+ // init + git add -A is already clean under core.autocrlf=true (#2118 / #3013).
190
+ syncBareVersionMarker(projectDir, version);
191
+ removeStaleMigratedFrameworkNarrative(projectDir);
157
192
  writeConsumerGitHooks(projectDir, deftDir, io, seams.gitHooks);
158
193
  writeAgentHookDeposit(projectDir, io);
159
194
  let taskfileWired = false;
@@ -1,4 +1,4 @@
1
- import { type CompletedProcess } from "../scm/call.js";
1
+ import type { CompletedProcess } from "../scm/call.js";
2
2
  import { probeRuntimeCapabilities, type RuntimeCapabilityReport } from "./platform-capabilities.js";
3
3
  export declare const GITHUB_AUTH_MODE_INJECTED_TOKEN = "injected-token";
4
4
  export declare const GITHUB_AUTH_MODE_HOST_GH = "host-gh";
@@ -1,4 +1,4 @@
1
- import { call } from "../scm/call.js";
1
+ import { spawnSync } from "node:child_process";
2
2
  import { pyRepr } from "../scm/py-format.js";
3
3
  import { getPlatformCapabilities, probeRuntimeCapabilities, RUNTIME_MODE_CLOUD_HEADLESS, RUNTIME_MODE_CURSOR_NATIVE_SANDBOX, } from "./platform-capabilities.js";
4
4
  export const GITHUB_AUTH_MODE_INJECTED_TOKEN = "injected-token";
@@ -37,9 +37,31 @@ export function inferGithubAuthMode(runtimeReport) {
37
37
  }
38
38
  return GITHUB_AUTH_MODE_HOST_GH;
39
39
  }
40
+ /**
41
+ * Prefer live `gh` for auth/API validation. Do not route through scm.call /
42
+ * BINARY_PREFERENCE (ghx-first): ghx is a cached GET proxy and rejects multi-arg
43
+ * `api user --jq .login` forms used here (#2275 Greptile P1 / #954).
44
+ */
40
45
  function defaultRunGh(args, environ) {
41
- const verb = args[0];
42
- return call("github-issue", verb, args.slice(1), { env: environ, timeout: 30 });
46
+ const binary = "gh";
47
+ try {
48
+ const result = spawnSync(binary, [...args], {
49
+ env: environ,
50
+ encoding: "utf8",
51
+ timeout: 30_000,
52
+ stdio: ["ignore", "pipe", "pipe"],
53
+ });
54
+ return {
55
+ args: [binary, ...args],
56
+ returncode: result.status ?? 1,
57
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
58
+ stderr: typeof result.stderr === "string" ? result.stderr : "",
59
+ };
60
+ }
61
+ catch (err) {
62
+ const message = err instanceof Error ? err.message : String(err);
63
+ return { args: [binary, ...args], returncode: 1, stdout: "", stderr: message };
64
+ }
43
65
  }
44
66
  function splitRepo(repo) {
45
67
  const idx = repo.indexOf("/");
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { fileURLToPath } from "node:url";
3
+ import { ScmStubError } from "../scm/errors.js";
4
+ import { requireScmReady } from "../scm/readiness.js";
3
5
  import { issueIngestMain } from "./issue-ingest.js";
4
6
  function parseArgs(argv) {
5
7
  const out = {};
@@ -25,6 +27,17 @@ function parseArgs(argv) {
25
27
  return out;
26
28
  }
27
29
  export function mainEntry(argv = process.argv.slice(2)) {
30
+ // #2275: fail loud when gh/auth is missing in this execution env.
31
+ try {
32
+ requireScmReady();
33
+ }
34
+ catch (err) {
35
+ if (err instanceof ScmStubError) {
36
+ process.stderr.write(`error: ${err.message}\n`);
37
+ return 2;
38
+ }
39
+ throw err;
40
+ }
28
41
  return issueIngestMain(parseArgs(argv));
29
42
  }
30
43
  if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { fileURLToPath } from "node:url";
3
+ import { ScmStubError } from "../scm/errors.js";
4
+ import { requireScmReady } from "../scm/readiness.js";
3
5
  import { reconcileMain } from "./reconcile-issues.js";
4
6
  function parseArgs(argv) {
5
7
  const out = {};
@@ -23,6 +25,17 @@ function parseArgs(argv) {
23
25
  return out;
24
26
  }
25
27
  export function mainEntry(argv = process.argv.slice(2)) {
28
+ // #2275: fail loud when gh/auth is missing in this execution env.
29
+ try {
30
+ requireScmReady();
31
+ }
32
+ catch (err) {
33
+ if (err instanceof ScmStubError) {
34
+ process.stderr.write(`error: ${err.message}\n`);
35
+ return 2;
36
+ }
37
+ throw err;
38
+ }
26
39
  return reconcileMain(parseArgs(argv));
27
40
  }
28
41
  if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
@@ -6,11 +6,13 @@ import { contentRoot } from "../content-root.js";
6
6
  import { ATTRIBUTION_REQUIRED_PAYLOAD } from "../events/attribution-constants.js";
7
7
  import { containedWrite } from "../fs/contained-write.js";
8
8
  import { ProjectionContainmentError } from "../fs/projection-containment.js";
9
+ import { PROCESS_COST_REQUIRED_PAYLOAD } from "../session/process-cost-constants.js";
9
10
  /** Default event log location (project-local). */
10
11
  export const DEFAULT_EVENT_LOG = join(".deft-cache", "events.jsonl");
11
12
  const BEHAVIORAL_CATEGORY = "behavioral";
12
13
  const REQUIRED_BEHAVIORAL_PAYLOAD = {
13
14
  ...ATTRIBUTION_REQUIRED_PAYLOAD,
15
+ ...PROCESS_COST_REQUIRED_PAYLOAD,
14
16
  "session:interrupted": ["session_id", "reason"],
15
17
  "session:resumed": ["session_id", "interrupted_id"],
16
18
  "plan:approved": ["plan_ref", "approver"],
@@ -2,4 +2,5 @@ export * from "./event.js";
2
2
  export * as eventDetect from "./event-detect.js";
3
3
  export * as events from "./events.js";
4
4
  export * as lifecycleHygiene from "./lifecycle-hygiene.js";
5
+ export * from "./stats.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -2,4 +2,5 @@ export * from "./event.js";
2
2
  export * as eventDetect from "./event-detect.js";
3
3
  export * as events from "./events.js";
4
4
  export * as lifecycleHygiene from "./lifecycle-hygiene.js";
5
+ export * from "./stats.js";
5
6
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,53 @@
1
+ import { parseDurationMs } from "../triage/scope/duration.js";
2
+ /** Lifecycle folders scanned for stats. */
3
+ export declare const STATS_LIFECYCLE_FOLDERS: readonly ["proposed", "pending", "active", "completed", "cancelled"];
4
+ export type StatsLifecycleFolder = (typeof STATS_LIFECYCLE_FOLDERS)[number];
5
+ /** Count semantics for WWYSYDH / process forms (stable strings for --json). */
6
+ export declare const LIFECYCLE_STATS_SEMANTICS: {
7
+ readonly promoted: string;
8
+ readonly activated: string;
9
+ readonly completed: string;
10
+ readonly cancelled_or_failed: string;
11
+ readonly still_active: "snapshot count of all xbriefs currently in active/ (not filtered by --since).";
12
+ readonly event_time: string;
13
+ readonly note: string;
14
+ };
15
+ export interface LifecycleFolderTotals {
16
+ readonly proposed: number;
17
+ readonly pending: number;
18
+ readonly active: number;
19
+ readonly completed: number;
20
+ readonly cancelled: number;
21
+ }
22
+ export interface LifecycleStats {
23
+ readonly since: string;
24
+ readonly since_ms: number;
25
+ readonly as_of: string;
26
+ readonly window_start: string;
27
+ readonly project_root: string;
28
+ readonly lifecycle_root: string;
29
+ readonly promoted: number;
30
+ readonly activated: number;
31
+ readonly completed: number;
32
+ readonly cancelled_or_failed: number;
33
+ readonly still_active: number;
34
+ readonly folder_totals: LifecycleFolderTotals;
35
+ readonly semantics: typeof LIFECYCLE_STATS_SEMANTICS;
36
+ }
37
+ export interface CollectLifecycleStatsOptions {
38
+ readonly projectRoot: string;
39
+ /** Window duration string, e.g. "7d", "24h", "1w". Default "7d". */
40
+ readonly since?: string;
41
+ /** Clock for window end (tests inject a fixed Date). */
42
+ readonly now?: Date;
43
+ }
44
+ /**
45
+ * Collect lifecycle folder stats for a project root.
46
+ * Offline / filesystem only.
47
+ */
48
+ export declare function collectLifecycleStats(options: CollectLifecycleStatsOptions): LifecycleStats;
49
+ /** Format human-readable lifecycle stats text. */
50
+ export declare function formatLifecycleStatsText(stats: LifecycleStats): string;
51
+ /** Re-export duration parse for CLI error messaging. */
52
+ export { parseDurationMs };
53
+ //# sourceMappingURL=stats.d.ts.map