@brainervirus/workit-core 0.10.0 → 1.0.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 (150) hide show
  1. package/README.md +5 -3
  2. package/package.json +8 -3
  3. package/scripts/analyze-release-scope.ts +4 -1
  4. package/scripts/doctor-check.ts +3 -1
  5. package/scripts/install-codex-plugin.sh +28 -0
  6. package/scripts/install-cursor-plugin.sh +1 -0
  7. package/scripts/install-opencode-plugin.sh +1 -0
  8. package/scripts/install-pi-package.sh +56 -0
  9. package/scripts/rewrite-workspace-deps.ts +22 -5
  10. package/scripts/sync-release-manifests.ts +8 -5
  11. package/scripts/sync-runtime.sh +7 -3
  12. package/scripts/validate-cursor-marketplace.ts +16 -62
  13. package/skills/workit-babysit/SKILL.md +33 -0
  14. package/skills/workit-behavioral-tdd/SKILL.md +53 -0
  15. package/skills/workit-blast-radius/SKILL.md +31 -0
  16. package/skills/workit-challenge/SKILL.md +62 -0
  17. package/skills/workit-debug/SKILL.md +61 -0
  18. package/skills/workit-deslop/SKILL.md +36 -0
  19. package/skills/workit-diagram/SKILL.md +32 -0
  20. package/skills/workit-green-run/SKILL.md +29 -0
  21. package/skills/workit-handoff/SKILL.md +43 -0
  22. package/skills/workit-implement/SKILL.md +46 -0
  23. package/skills/workit-mockup/SKILL.md +28 -0
  24. package/skills/workit-plan/SKILL.md +66 -0
  25. package/skills/workit-review/SKILL.md +60 -0
  26. package/skills/workit-steer/SKILL.md +32 -0
  27. package/src/core/authority.ts +918 -0
  28. package/src/core/boundary.ts +29 -0
  29. package/src/core/branch.ts +26 -255
  30. package/src/core/changelog.ts +95 -18
  31. package/src/core/commit-flavors.ts +65 -0
  32. package/src/core/config-conversion.ts +183 -0
  33. package/src/core/config.ts +42 -0
  34. package/src/core/cutover.ts +721 -0
  35. package/src/core/detect-hosts.ts +107 -0
  36. package/src/core/doctor.ts +428 -18
  37. package/src/core/external-action-effects.ts +1171 -0
  38. package/src/core/external-action.ts +541 -0
  39. package/src/core/init.ts +83 -32
  40. package/src/core/methods.ts +125 -0
  41. package/src/core/policy-resolver.ts +523 -0
  42. package/src/core/pr-create.ts +7 -1
  43. package/src/core/registration.ts +69 -4
  44. package/src/core/repo-context.ts +59 -1
  45. package/src/core/setup.ts +28 -0
  46. package/src/core/skill-manifests.ts +40 -42
  47. package/src/core/support-matrix.ts +8 -3
  48. package/src/core/sync-runtime.ts +15 -3
  49. package/src/core/task-context.ts +179 -0
  50. package/src/core/task-contract.ts +1136 -0
  51. package/src/core/task-engine.ts +2562 -0
  52. package/src/core/task-evaluation.ts +679 -0
  53. package/src/core/task-store.ts +1071 -0
  54. package/src/core/tracker-issues.ts +237 -0
  55. package/src/core/uninstall.ts +67 -3
  56. package/src/core/vcs-config.ts +53 -22
  57. package/src/core/workers.ts +166 -0
  58. package/src/core/workspaces.ts +3 -1
  59. package/src/core/youtrack-tools.ts +2 -0
  60. package/src/core/youtrack.ts +145 -10
  61. package/src/core.ts +126 -0
  62. package/templates/execution-contract.md +17 -50
  63. package/templates/plan-template.md +3 -3
  64. package/templates/spec-template.md +15 -3
  65. package/templates/workit-contract.md +12 -0
  66. package/commands/wk-changelog.md +0 -2
  67. package/commands/wk-commit.md +0 -2
  68. package/commands/wk-docs-refresh.md +0 -2
  69. package/commands/wk-handoff.md +0 -2
  70. package/commands/wk-implement.md +0 -2
  71. package/commands/wk-init.md +0 -2
  72. package/commands/wk-issue-update.md +0 -2
  73. package/commands/wk-meetings.md +0 -2
  74. package/commands/wk-pr.md +0 -2
  75. package/commands/wk-release-notes.md +0 -2
  76. package/commands/wk-status.md +0 -2
  77. package/commands/wk-verify.md +0 -2
  78. package/scripts/update-superpowers.sh +0 -82
  79. package/scripts/vendor-assets.ts +0 -37
  80. package/skills/wk-changelog/SKILL.md +0 -15
  81. package/skills/wk-commit/SKILL.md +0 -16
  82. package/skills/wk-docs-refresh/SKILL.md +0 -15
  83. package/skills/wk-handoff/SKILL.md +0 -20
  84. package/skills/wk-implement/SKILL.md +0 -47
  85. package/skills/wk-init/SKILL.md +0 -31
  86. package/skills/wk-issue-update/SKILL.md +0 -27
  87. package/skills/wk-issue-update/references/youtrack-update-style.md +0 -81
  88. package/skills/wk-meetings/SKILL.md +0 -17
  89. package/skills/wk-pr/SKILL.md +0 -27
  90. package/skills/wk-release-notes/SKILL.md +0 -15
  91. package/skills/wk-status/SKILL.md +0 -16
  92. package/skills/wk-verify/SKILL.md +0 -16
  93. package/src/core/detector.ts +0 -239
  94. package/src/core/flow-state.ts +0 -3048
  95. package/src/core/handoff-context.ts +0 -136
  96. package/src/core/handoff-tools.ts +0 -133
  97. package/src/core/menu.ts +0 -70
  98. package/src/core/plan-tasks.ts +0 -43
  99. package/src/core/reminder.ts +0 -124
  100. package/src/core/sdd.ts +0 -353
  101. package/src/state.ts +0 -22
  102. package/templates/superpowers-doc-contract.md +0 -75
  103. package/vendor/superpowers/skills/brainstorming/SKILL.md +0 -159
  104. package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +0 -213
  105. package/vendor/superpowers/skills/brainstorming/scripts/helper.js +0 -167
  106. package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +0 -723
  107. package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +0 -209
  108. package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +0 -120
  109. package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +0 -49
  110. package/vendor/superpowers/skills/brainstorming/visual-companion.md +0 -291
  111. package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +0 -185
  112. package/vendor/superpowers/skills/executing-plans/SKILL.md +0 -70
  113. package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +0 -241
  114. package/vendor/superpowers/skills/receiving-code-review/SKILL.md +0 -213
  115. package/vendor/superpowers/skills/requesting-code-review/SKILL.md +0 -103
  116. package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +0 -172
  117. package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +0 -426
  118. package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +0 -139
  119. package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +0 -44
  120. package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +0 -22
  121. package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +0 -40
  122. package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +0 -188
  123. package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +0 -119
  124. package/vendor/superpowers/skills/systematic-debugging/SKILL.md +0 -296
  125. package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
  126. package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +0 -115
  127. package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +0 -122
  128. package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +0 -63
  129. package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +0 -169
  130. package/vendor/superpowers/skills/systematic-debugging/test-academic.md +0 -14
  131. package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +0 -58
  132. package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +0 -68
  133. package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +0 -69
  134. package/vendor/superpowers/skills/test-driven-development/SKILL.md +0 -371
  135. package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +0 -299
  136. package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +0 -202
  137. package/vendor/superpowers/skills/using-superpowers/SKILL.md +0 -62
  138. package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +0 -23
  139. package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +0 -39
  140. package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +0 -16
  141. package/vendor/superpowers/skills/verification-before-completion/SKILL.md +0 -139
  142. package/vendor/superpowers/skills/writing-plans/SKILL.md +0 -174
  143. package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
  144. package/vendor/superpowers/skills/writing-skills/SKILL.md +0 -689
  145. package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +0 -1150
  146. package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +0 -189
  147. package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +0 -172
  148. package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +0 -187
  149. package/vendor/superpowers/skills/writing-skills/render-graphs.js +0 -168
  150. package/vendor/superpowers/skills/writing-skills/testing-skills-with-subagents.md +0 -384
@@ -0,0 +1,523 @@
1
+ import * as z from "zod";
2
+ import {
3
+ assessmentSchema,
4
+ canonicalJson,
5
+ constraintSchema,
6
+ decisionSchema,
7
+ failure,
8
+ findingSchema,
9
+ intentSchema,
10
+ POLICY_VERSION,
11
+ policyChangeSchema,
12
+ policySchema,
13
+ requirementId,
14
+ requirementSchema,
15
+ sha256,
16
+ success,
17
+ type Assessment,
18
+ type Constraint,
19
+ type Decision,
20
+ type Dimension,
21
+ type Finding,
22
+ type Intent,
23
+ type Policy,
24
+ type PolicyChange,
25
+ type Requirement,
26
+ type Result,
27
+ type Scope,
28
+ } from "./task-contract";
29
+
30
+ export type ResolverPrior = {
31
+ decisions: Decision[];
32
+ findings: Finding[];
33
+ requirements: Requirement[];
34
+ };
35
+
36
+ export type ResolverPreferences = {
37
+ fast?: boolean;
38
+ thorough?: boolean;
39
+ };
40
+
41
+ /** Size facts for deterministic spec triage (op1 with override). */
42
+ export type TriageFacts = {
43
+ /** New or changed observable behavior. */
44
+ newOrChangedBehavior: boolean;
45
+ /** Open ambiguity needing user resolution. */
46
+ ambiguityOpen: boolean;
47
+ /** Cross-package/host contract, auth/data/security surface, or irreversible migration. */
48
+ crossContract: boolean;
49
+ /** Subsystems touched. */
50
+ subsystems: number;
51
+ /** Packages touched (a 2-package change is Large even in one subsystem). */
52
+ packagesTouched: number;
53
+ /** Estimated implementation steps. */
54
+ steps: number;
55
+ };
56
+
57
+ export type TriageTier = "spec-plan" | "plan-only" | "neither";
58
+
59
+ /** Large → spec + full plan; Medium → compact plan-only; Small → neither.
60
+ * Step count alone never escalates: a long but known, single-subsystem run
61
+ * stays plan-only. The lead may re-tier with a reason recorded in progress
62
+ * (override). */
63
+ export const triageTier = (facts: TriageFacts): TriageTier => {
64
+ if (
65
+ facts.newOrChangedBehavior ||
66
+ facts.ambiguityOpen ||
67
+ facts.crossContract ||
68
+ facts.subsystems >= 3 ||
69
+ facts.packagesTouched >= 2
70
+ )
71
+ return "spec-plan";
72
+ if (facts.steps >= 2) return "plan-only";
73
+ return "neither";
74
+ };
75
+
76
+ /** Tier → assessor signal values (durableAgreementNeeded, coordinationPlanNeeded). */
77
+ export const triageSignals = (
78
+ tier: TriageTier,
79
+ ): { durableAgreementNeeded: boolean; coordinationPlanNeeded: boolean } =>
80
+ tier === "spec-plan"
81
+ ? { durableAgreementNeeded: true, coordinationPlanNeeded: true }
82
+ : tier === "plan-only"
83
+ ? { durableAgreementNeeded: false, coordinationPlanNeeded: true }
84
+ : { durableAgreementNeeded: false, coordinationPlanNeeded: false };
85
+
86
+ export type ResolverInput = {
87
+ intent: Intent;
88
+ assessment: Assessment;
89
+ constraints: Constraint[];
90
+ prior: ResolverPrior;
91
+ preferences?: ResolverPreferences;
92
+ };
93
+
94
+ type NormalizedResolverInput = ResolverInput & { policyVersion: typeof POLICY_VERSION };
95
+
96
+ const resolverInputSchema = z
97
+ .object({
98
+ intent: intentSchema,
99
+ assessment: assessmentSchema,
100
+ constraints: z.array(constraintSchema),
101
+ prior: z
102
+ .object({
103
+ decisions: z.array(decisionSchema),
104
+ findings: z.array(findingSchema),
105
+ requirements: z.array(requirementSchema),
106
+ })
107
+ .strict(),
108
+ preferences: z
109
+ .object({ fast: z.boolean().optional(), thorough: z.boolean().optional() })
110
+ .strict()
111
+ .optional(),
112
+ })
113
+ .strict();
114
+
115
+ const compareCodeUnits = (left: string, right: string): number => {
116
+ const length = Math.min(left.length, right.length);
117
+ for (let index = 0; index < length; index += 1) {
118
+ const difference = left.charCodeAt(index) - right.charCodeAt(index);
119
+ if (difference !== 0) return difference;
120
+ }
121
+ return left.length - right.length;
122
+ };
123
+
124
+ const stableUnique = (values: string[]): string[] => [...new Set(values)].sort(compareCodeUnits);
125
+
126
+ const stableList = <T>(values: T[]): T[] =>
127
+ [...values].sort((left, right) => compareCodeUnits(canonicalJson(left), canonicalJson(right)));
128
+
129
+ const normalizedScope = (scope: Scope): Scope => ({
130
+ description: scope.description,
131
+ paths: stableUnique(scope.paths),
132
+ exclusions: stableUnique(scope.exclusions),
133
+ });
134
+
135
+ const normalizedRefList = <T>(values: T[]): T[] => stableList(values);
136
+
137
+ const normalizeAssessment = (assessment: Assessment): Assessment => ({
138
+ facts: stableList(
139
+ assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) })),
140
+ ),
141
+ signals: Object.fromEntries(
142
+ Object.entries(assessment.signals).map(([name, signal]) => [
143
+ name,
144
+ { ...signal, refs: normalizedRefList(signal.refs) },
145
+ ]),
146
+ ) as Assessment["signals"],
147
+ consequences: stableList(
148
+ assessment.consequences.map((consequence) => ({
149
+ ...consequence,
150
+ fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) },
151
+ })),
152
+ ),
153
+ verification: stableList(
154
+ assessment.verification.map((verification) => ({
155
+ ...verification,
156
+ scope: normalizedScope(verification.scope),
157
+ availableChecks: normalizedRefList(verification.availableChecks),
158
+ gaps: stableUnique(verification.gaps),
159
+ })),
160
+ ),
161
+ });
162
+
163
+ const normalizeConstraint = (constraint: Constraint): Constraint => ({
164
+ ...constraint,
165
+ source: constraint.source,
166
+ requires: stableList(
167
+ constraint.requires.map((requirement) => ({
168
+ ...requirement,
169
+ scope: normalizedScope(requirement.scope),
170
+ refs: normalizedRefList(requirement.refs),
171
+ })),
172
+ ),
173
+ });
174
+
175
+ const normalizeDecision = (decision: Decision): Decision => ({
176
+ ...decision,
177
+ binding: {
178
+ ...decision.binding,
179
+ scope: normalizedScope(decision.binding.scope),
180
+ contentRefs: normalizedRefList(decision.binding.contentRefs),
181
+ },
182
+ requirementIds: stableUnique(decision.requirementIds),
183
+ });
184
+
185
+ const normalizeIntent = (intent: Intent): Intent => ({
186
+ ...intent,
187
+ scope: normalizedScope(intent.scope),
188
+ authorityRefs: normalizedRefList(intent.authorityRefs),
189
+ });
190
+
191
+ const normalizePrior = (prior: ResolverPrior): ResolverPrior => ({
192
+ decisions: stableList(prior.decisions.map(normalizeDecision)),
193
+ findings: stableList(
194
+ prior.findings.map((finding) => ({
195
+ ...finding,
196
+ scope: normalizedScope(finding.scope),
197
+ refs: normalizedRefList(finding.refs),
198
+ resolution: finding.resolution
199
+ ? {
200
+ ...finding.resolution,
201
+ evidenceIds: stableUnique(finding.resolution.evidenceIds),
202
+ decisionIds: stableUnique(finding.resolution.decisionIds),
203
+ }
204
+ : null,
205
+ })),
206
+ ),
207
+ requirements: stableList(
208
+ prior.requirements.map((requirement) => ({
209
+ ...requirement,
210
+ scope: normalizedScope(requirement.scope),
211
+ })),
212
+ ),
213
+ });
214
+
215
+ const invalidFields = (error: z.ZodError) => ({
216
+ fields: error.issues.map((issue) => ({
217
+ path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
218
+ reason: issue.message,
219
+ })),
220
+ });
221
+
222
+ function normalizeResolverInput(input: unknown): Result<NormalizedResolverInput> {
223
+ const parsed = resolverInputSchema.safeParse(input);
224
+ if (!parsed.success)
225
+ return failure("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
226
+
227
+ const value = parsed.data;
228
+ const signals = value.assessment.signals;
229
+ if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
230
+ return failure("invalid_input", "behavioral change cannot be wholly mechanical", {
231
+ fields: [
232
+ {
233
+ path: "assessment.signals",
234
+ reason: "contradictory behaviorChange and mechanicalLowRisk",
235
+ },
236
+ ],
237
+ });
238
+ const ids = value.constraints.map((constraint) => constraint.id);
239
+ if (new Set(ids).size !== ids.length)
240
+ return failure("invalid_input", "constraint IDs must be unique", {
241
+ fields: [{ path: "constraints", reason: "duplicate constraint id" }],
242
+ });
243
+ if (value.preferences?.fast && value.preferences.thorough)
244
+ return failure("invalid_input", "fast and thorough preferences contradict", {
245
+ fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }],
246
+ });
247
+
248
+ const normalized: NormalizedResolverInput = {
249
+ intent: normalizeIntent(value.intent),
250
+ assessment: normalizeAssessment(value.assessment),
251
+ constraints: stableList(value.constraints.map(normalizeConstraint)),
252
+ prior: normalizePrior(value.prior),
253
+ ...(value.preferences ? { preferences: { ...value.preferences } } : {}),
254
+ policyVersion: POLICY_VERSION,
255
+ };
256
+ return success(null, null, normalized);
257
+ }
258
+
259
+ type RequirementSpec = {
260
+ ruleId: string;
261
+ dimension: Dimension;
262
+ scope: Scope;
263
+ reason: string;
264
+ satisfaction: string;
265
+ before: Requirement["before"];
266
+ dependentAction: string | null;
267
+ acceptanceAllowed: boolean;
268
+ };
269
+
270
+ const requirement = (spec: RequirementSpec): Requirement => ({
271
+ ...spec,
272
+ id: requirementId({
273
+ ruleId: spec.ruleId,
274
+ scope: spec.scope,
275
+ satisfaction: spec.satisfaction,
276
+ before: spec.before,
277
+ dependentAction: spec.dependentAction,
278
+ }),
279
+ });
280
+
281
+ const signalReason = (input: NormalizedResolverInput, name: keyof Assessment["signals"]): string =>
282
+ input.assessment.signals[name].reason;
283
+
284
+ function resolveRequirements(input: NormalizedResolverInput): Requirement[] {
285
+ const { signals } = input.assessment;
286
+ const scope = input.intent.scope;
287
+ const requirements: Requirement[] = [];
288
+ const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
289
+ const hasUnknownFact = [
290
+ ...input.assessment.facts,
291
+ ...input.assessment.consequences.map(({ fact }) => fact),
292
+ ].some((fact) => fact.basis === "unknown");
293
+
294
+ if (hasUnknownSignal || hasUnknownFact)
295
+ requirements.push(
296
+ requirement({
297
+ ruleId: "consequential-unknown",
298
+ dimension: "investigation",
299
+ scope,
300
+ reason: `Resolve consequential uncertainty before the dependent action: ${[
301
+ ...Object.entries(signals)
302
+ .filter(([, signal]) => signal.value === "unknown")
303
+ .map(([name]) => name),
304
+ ...(hasUnknownFact ? ["unknown fact"] : []),
305
+ ].join(", ")}`,
306
+ satisfaction:
307
+ "An observed or inferred fact resolves the uncertainty and records its supporting references.",
308
+ before: "dependent_action",
309
+ dependentAction: "dependent-action",
310
+ acceptanceAllowed: false,
311
+ }),
312
+ );
313
+
314
+ if (signals.productChoiceOpen.value === true)
315
+ requirements.push(
316
+ requirement({
317
+ ruleId: "product-decision",
318
+ dimension: "decisions",
319
+ scope,
320
+ reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
321
+ satisfaction:
322
+ "Present the material alternatives and record the user's decision before the dependent action.",
323
+ before: "dependent_action",
324
+ dependentAction: "dependent-action",
325
+ acceptanceAllowed: false,
326
+ }),
327
+ );
328
+
329
+ if (signals.behaviorChange.value === true)
330
+ requirements.push(
331
+ requirement({
332
+ ruleId: "behavioral-verification",
333
+ dimension: "testing",
334
+ scope,
335
+ reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
336
+ satisfaction:
337
+ "A useful behavioral check passes against the affected behavior and current candidate.",
338
+ before: "close",
339
+ dependentAction: null,
340
+ acceptanceAllowed: false,
341
+ }),
342
+ requirement({
343
+ ruleId: "fresh-context-review",
344
+ dimension: "review",
345
+ scope,
346
+ reason: "A non-mechanical behavior change requires fresh-context review.",
347
+ satisfaction:
348
+ "A separate review context examines the current candidate and records its result.",
349
+ before: "close",
350
+ dependentAction: null,
351
+ acceptanceAllowed: false,
352
+ }),
353
+ );
354
+ else if (signals.mechanicalLowRisk.value === true)
355
+ requirements.push(
356
+ requirement({
357
+ ruleId: "mechanical-existing-checks",
358
+ dimension: "verification",
359
+ scope,
360
+ reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
361
+ satisfaction:
362
+ "Run the relevant existing checks for affected consumers; do not add tautological tests.",
363
+ before: "close",
364
+ dependentAction: null,
365
+ acceptanceAllowed: true,
366
+ }),
367
+ requirement({
368
+ ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
369
+ dimension: "review",
370
+ scope,
371
+ reason: input.preferences?.thorough
372
+ ? "The thorough preference upgrades mechanical self-review to fresh-context review."
373
+ : "Mechanical low-risk work uses self-review rather than mandatory fresh-context review.",
374
+ satisfaction: input.preferences?.thorough
375
+ ? "A separate review context examines the current candidate and records its result."
376
+ : "The lead reviews the resulting diff and records the relevant scope and checks.",
377
+ before: "close",
378
+ dependentAction: null,
379
+ acceptanceAllowed: !input.preferences?.thorough,
380
+ }),
381
+ );
382
+
383
+ if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
384
+ requirements.push(
385
+ requirement({
386
+ ruleId: "pre-pr-cleanup",
387
+ dimension: "verification",
388
+ scope,
389
+ reason: "Implementation output must be deslopped before it is delivered.",
390
+ satisfaction:
391
+ "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
392
+ before: "dependent_action",
393
+ dependentAction: "hosting.pull_request",
394
+ acceptanceAllowed: true,
395
+ }),
396
+ );
397
+
398
+ if (signals.durableAgreementNeeded.value === true)
399
+ requirements.push(
400
+ requirement({
401
+ ruleId: "durable-spec",
402
+ dimension: "artifacts",
403
+ scope,
404
+ reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
405
+ satisfaction:
406
+ "Record the agreed behavior in a durable specification before implementation writes.",
407
+ before: "write",
408
+ dependentAction: null,
409
+ acceptanceAllowed: true,
410
+ }),
411
+ );
412
+ if (signals.coordinationPlanNeeded.value === true)
413
+ requirements.push(
414
+ requirement({
415
+ ruleId: "coordination-plan",
416
+ dimension: "continuity",
417
+ scope,
418
+ reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
419
+ satisfaction:
420
+ "Record useful sequencing, dependencies, and the next action in a compact plan.",
421
+ before: "dependent_action",
422
+ dependentAction: null,
423
+ acceptanceAllowed: true,
424
+ }),
425
+ );
426
+ if (signals.helperUseful.value === true && !input.preferences?.fast)
427
+ requirements.push(
428
+ requirement({
429
+ ruleId: "helper-usefulness",
430
+ dimension: "delegation",
431
+ scope,
432
+ reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
433
+ satisfaction:
434
+ "Use a bounded helper only for the independent objective and reconcile its result.",
435
+ before: "dependent_action",
436
+ dependentAction: null,
437
+ acceptanceAllowed: true,
438
+ }),
439
+ );
440
+
441
+ for (const constraint of input.constraints)
442
+ for (const obligation of constraint.requires) {
443
+ const dimension =
444
+ obligation.method ??
445
+ (obligation.kind === "check"
446
+ ? "verification"
447
+ : obligation.kind === "decision"
448
+ ? "decisions"
449
+ : "challenge");
450
+ requirements.push(
451
+ requirement({
452
+ ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
453
+ kind: obligation.kind,
454
+ scope: normalizedScope(obligation.scope),
455
+ refs: normalizedRefList(obligation.refs),
456
+ method: obligation.method,
457
+ before: obligation.before,
458
+ dependentAction: obligation.dependentAction,
459
+ })}`,
460
+ dimension,
461
+ scope: obligation.scope,
462
+ reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
463
+ satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
464
+ before: obligation.before,
465
+ dependentAction: obligation.dependentAction,
466
+ acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision",
467
+ }),
468
+ );
469
+ }
470
+
471
+ const seen = new Set<string>();
472
+ return requirements.filter((item) => {
473
+ if (seen.has(item.id)) return false;
474
+ seen.add(item.id);
475
+ return true;
476
+ });
477
+ }
478
+
479
+ export function resolvePolicy(input: ResolverInput): Result<Policy> {
480
+ const normalized = normalizeResolverInput(input);
481
+ if (!normalized.ok) return normalized;
482
+ const requirements = resolveRequirements(normalized.data);
483
+ const policy: Policy = {
484
+ policyVersion: POLICY_VERSION,
485
+ inputDigest: sha256(canonicalJson(normalized.data)),
486
+ requirements,
487
+ };
488
+ const parsed = policySchema.safeParse(policy);
489
+ if (!parsed.success)
490
+ return failure("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
491
+ return success(null, null, parsed.data);
492
+ }
493
+
494
+ export function diffPolicy(
495
+ previous: Policy | null,
496
+ next: Policy,
497
+ reason: string,
498
+ now: string,
499
+ ): PolicyChange | null {
500
+ if (!policySchema.safeParse(previous).success && previous !== null)
501
+ throw new TypeError("previous policy is invalid");
502
+ if (!policySchema.safeParse(next).success) throw new TypeError("next policy is invalid");
503
+ const oldIds = new Set(previous?.requirements.map((requirement) => requirement.id) ?? []);
504
+ const newIds = new Set(next.requirements.map((requirement) => requirement.id));
505
+ const added = [...newIds].filter((id) => !oldIds.has(id)).sort(compareCodeUnits);
506
+ const retired = [...oldIds].filter((id) => !newIds.has(id)).sort(compareCodeUnits);
507
+ if (added.length === 0 && retired.length === 0) return null;
508
+ const normalizedReason = reason.trim();
509
+ const changeReason = retired.length
510
+ ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})`
511
+ : normalizedReason || "policy reassessed";
512
+ const change = {
513
+ recordedAt: now,
514
+ fromInputDigest: previous?.inputDigest ?? null,
515
+ toInputDigest: next.inputDigest,
516
+ added,
517
+ retired,
518
+ reason: changeReason,
519
+ } satisfies PolicyChange;
520
+ const parsed = policyChangeSchema.safeParse(change);
521
+ if (!parsed.success) throw new TypeError("policy change is invalid");
522
+ return parsed.data;
523
+ }
@@ -100,6 +100,10 @@ function whichOnPath(tool: string): string | null {
100
100
  return null;
101
101
  }
102
102
 
103
+ /** Whether the configured hosting provider's native CLI is available. */
104
+ export const hostingCliAvailable = (provider: string): boolean =>
105
+ whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
106
+
103
107
  function repoRoot(cwd: string): string {
104
108
  const result = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
105
109
  return result.status === 0 ? (result.stdout ?? "").trim() : cwd;
@@ -128,7 +132,9 @@ export function prBuildBody(env: NodeJS.ProcessEnv, cwd?: string): string {
128
132
 
129
133
  /** Port of scripts/pr-create.sh create mode — glab/gh MR/PR creation. */
130
134
  export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, any> {
131
- const root = process.env.WORKFLOW_WORKSPACE_ROOT ?? repoRoot(cwd);
135
+ // An explicitly supplied, already-approved checkout root outranks ambient
136
+ // process state; otherwise an unrelated env root could redirect the effect.
137
+ const root = cwd ? repoRoot(cwd) : (process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd());
132
138
  const policy = resolveBranchPolicyFor(root);
133
139
  const cfg = vcsConfig("load", root);
134
140
  if (!cfg.ok) return { error: cfg.error ?? "vcs config missing" };
@@ -161,22 +161,42 @@ export function mergeCursorMcp(
161
161
  return { config: base, changed };
162
162
  }
163
163
 
164
- /** Swap the sessionStart hook command, preserving other hooks and fields. */
164
+ /** Swap the sessionStart hook command and normalize the enforcement events to
165
+ * canonical (missing or divergent preToolUse/beforeShellExecution entries are
166
+ * rewritten so the installer heals exactly what the doctor flags). Other
167
+ * hooks and fields are preserved. */
165
168
  export function mergeCursorHooks(
166
169
  hooks: unknown,
167
170
  sessionStartEntry: Record<string, unknown>,
168
171
  ): MergeResult<Record<string, unknown>> {
169
172
  const base: Record<string, unknown> = isRecord(hooks) ? { ...hooks } : { version: 1 };
170
173
  const hooksMap = isRecord(base.hooks) ? { ...(base.hooks as Record<string, unknown>) } : {};
174
+ const changed: string[] = [];
171
175
  const list = Array.isArray(hooksMap.sessionStart) ? hooksMap.sessionStart : [];
172
176
  const same =
173
177
  list.length === 1 &&
174
178
  isRecord(list[0]) &&
175
179
  JSON.stringify(list[0]) === JSON.stringify(sessionStartEntry);
176
- if (same) return { config: base, changed: [] };
177
- hooksMap.sessionStart = [sessionStartEntry];
180
+ if (!same) {
181
+ hooksMap.sessionStart = [sessionStartEntry];
182
+ changed.push("hooks.sessionStart");
183
+ }
184
+ for (const event of ["preToolUse", "beforeShellExecution"] as const) {
185
+ const canonical = canonicalHookEntry(event);
186
+ const current = hooksMap[event];
187
+ if (
188
+ !Array.isArray(current) ||
189
+ current.length !== 1 ||
190
+ !isRecord(current[0]) ||
191
+ JSON.stringify({ ...current[0], failClosed: true }) !==
192
+ JSON.stringify({ ...canonical, failClosed: true })
193
+ ) {
194
+ hooksMap[event] = [canonical];
195
+ changed.push(`hooks.${event}`);
196
+ }
197
+ }
178
198
  base.hooks = hooksMap;
179
- return { config: base, changed: ["hooks.sessionStart"] };
199
+ return { config: base, changed };
180
200
  }
181
201
 
182
202
  /**
@@ -188,6 +208,51 @@ export function mergeCursorHooks(
188
208
  */
189
209
  export const CURSOR_RUNTIME_PACKAGE = "@brainervirus/workit-cursor@latest";
190
210
 
211
+ /**
212
+ * Canonical Cursor hook launcher (single source of truth for the shipped
213
+ * hooks-cursor.json, the installer merge, and the doctor drift check).
214
+ */
215
+ export const CURSOR_HOOK_RUN_COMMAND = `npx -y --prefer-online --package=${CURSOR_RUNTIME_PACKAGE} workit-cursor-hook`;
216
+
217
+ /**
218
+ * preToolUse matcher covering every name the hook's write-tool guard treats
219
+ * as a write (see CURSOR_WRITE_TOOL_NAMES in the Cursor hook). A narrower
220
+ * installed matcher lets write tools bypass enforcement silently.
221
+ */
222
+ export const CURSOR_PRETOOLUSE_MATCHER =
223
+ "Write|Edit|Delete|Shell|Remove|ApplyPatch|Apply_Patch|Patch|Rename|Mkdir|Mv|Cp|Touch";
224
+
225
+ const canonicalHookEntry = (
226
+ event: "preToolUse" | "beforeShellExecution",
227
+ ): Record<string, unknown> =>
228
+ event === "preToolUse"
229
+ ? { command: CURSOR_HOOK_RUN_COMMAND, matcher: CURSOR_PRETOOLUSE_MATCHER, failClosed: true }
230
+ : { command: CURSOR_HOOK_RUN_COMMAND, failClosed: true };
231
+
232
+ /**
233
+ * Drift between an installed hooks file and the canonical event entries.
234
+ * Only present-but-divergent entries count: absent events are filled in by
235
+ * the installer merge on the next run, and minimal installs predate them.
236
+ */
237
+ export function cursorHookDrift(installed: unknown): string[] {
238
+ if (!isRecord(installed) || !isRecord(installed.hooks)) return ["hooks file is not a hook map"];
239
+ const hooks = installed.hooks as Record<string, unknown>;
240
+ const drift: string[] = [];
241
+ for (const event of ["preToolUse", "beforeShellExecution"] as const) {
242
+ const list = hooks[event];
243
+ if (list === undefined) continue;
244
+ const canonical = canonicalHookEntry(event);
245
+ const entry = Array.isArray(list) ? list[0] : undefined;
246
+ if (
247
+ !isRecord(entry) ||
248
+ entry.command !== canonical.command ||
249
+ (event === "preToolUse" && entry.matcher !== canonical.matcher)
250
+ )
251
+ drift.push(event);
252
+ }
253
+ return drift;
254
+ }
255
+
191
256
  /**
192
257
  * Portable Cursor MCP server entry (CA-16/CA-17): launch the published package
193
258
  * through npx against its npm bin, so the Marketplace plugin never depends on a