@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.
- package/README.md +5 -3
- package/package.json +8 -3
- package/scripts/analyze-release-scope.ts +4 -1
- package/scripts/doctor-check.ts +3 -1
- package/scripts/install-codex-plugin.sh +28 -0
- package/scripts/install-cursor-plugin.sh +1 -0
- package/scripts/install-opencode-plugin.sh +1 -0
- package/scripts/install-pi-package.sh +56 -0
- package/scripts/rewrite-workspace-deps.ts +22 -5
- package/scripts/sync-release-manifests.ts +8 -5
- package/scripts/sync-runtime.sh +7 -3
- package/scripts/validate-cursor-marketplace.ts +16 -62
- package/skills/workit-babysit/SKILL.md +33 -0
- package/skills/workit-behavioral-tdd/SKILL.md +53 -0
- package/skills/workit-blast-radius/SKILL.md +31 -0
- package/skills/workit-challenge/SKILL.md +62 -0
- package/skills/workit-debug/SKILL.md +61 -0
- package/skills/workit-deslop/SKILL.md +36 -0
- package/skills/workit-diagram/SKILL.md +32 -0
- package/skills/workit-green-run/SKILL.md +29 -0
- package/skills/workit-handoff/SKILL.md +43 -0
- package/skills/workit-implement/SKILL.md +46 -0
- package/skills/workit-mockup/SKILL.md +28 -0
- package/skills/workit-plan/SKILL.md +66 -0
- package/skills/workit-review/SKILL.md +60 -0
- package/skills/workit-steer/SKILL.md +32 -0
- package/src/core/authority.ts +918 -0
- package/src/core/boundary.ts +29 -0
- package/src/core/branch.ts +26 -255
- package/src/core/changelog.ts +95 -18
- package/src/core/commit-flavors.ts +65 -0
- package/src/core/config-conversion.ts +183 -0
- package/src/core/config.ts +42 -0
- package/src/core/cutover.ts +721 -0
- package/src/core/detect-hosts.ts +107 -0
- package/src/core/doctor.ts +428 -18
- package/src/core/external-action-effects.ts +1171 -0
- package/src/core/external-action.ts +541 -0
- package/src/core/init.ts +83 -32
- package/src/core/methods.ts +125 -0
- package/src/core/policy-resolver.ts +523 -0
- package/src/core/pr-create.ts +7 -1
- package/src/core/registration.ts +69 -4
- package/src/core/repo-context.ts +59 -1
- package/src/core/setup.ts +28 -0
- package/src/core/skill-manifests.ts +40 -42
- package/src/core/support-matrix.ts +8 -3
- package/src/core/sync-runtime.ts +15 -3
- package/src/core/task-context.ts +179 -0
- package/src/core/task-contract.ts +1136 -0
- package/src/core/task-engine.ts +2562 -0
- package/src/core/task-evaluation.ts +679 -0
- package/src/core/task-store.ts +1071 -0
- package/src/core/tracker-issues.ts +237 -0
- package/src/core/uninstall.ts +67 -3
- package/src/core/vcs-config.ts +53 -22
- package/src/core/workers.ts +166 -0
- package/src/core/workspaces.ts +3 -1
- package/src/core/youtrack-tools.ts +2 -0
- package/src/core/youtrack.ts +145 -10
- package/src/core.ts +126 -0
- package/templates/execution-contract.md +17 -50
- package/templates/plan-template.md +3 -3
- package/templates/spec-template.md +15 -3
- package/templates/workit-contract.md +12 -0
- package/commands/wk-changelog.md +0 -2
- package/commands/wk-commit.md +0 -2
- package/commands/wk-docs-refresh.md +0 -2
- package/commands/wk-handoff.md +0 -2
- package/commands/wk-implement.md +0 -2
- package/commands/wk-init.md +0 -2
- package/commands/wk-issue-update.md +0 -2
- package/commands/wk-meetings.md +0 -2
- package/commands/wk-pr.md +0 -2
- package/commands/wk-release-notes.md +0 -2
- package/commands/wk-status.md +0 -2
- package/commands/wk-verify.md +0 -2
- package/scripts/update-superpowers.sh +0 -82
- package/scripts/vendor-assets.ts +0 -37
- package/skills/wk-changelog/SKILL.md +0 -15
- package/skills/wk-commit/SKILL.md +0 -16
- package/skills/wk-docs-refresh/SKILL.md +0 -15
- package/skills/wk-handoff/SKILL.md +0 -20
- package/skills/wk-implement/SKILL.md +0 -47
- package/skills/wk-init/SKILL.md +0 -31
- package/skills/wk-issue-update/SKILL.md +0 -27
- package/skills/wk-issue-update/references/youtrack-update-style.md +0 -81
- package/skills/wk-meetings/SKILL.md +0 -17
- package/skills/wk-pr/SKILL.md +0 -27
- package/skills/wk-release-notes/SKILL.md +0 -15
- package/skills/wk-status/SKILL.md +0 -16
- package/skills/wk-verify/SKILL.md +0 -16
- package/src/core/detector.ts +0 -239
- package/src/core/flow-state.ts +0 -3048
- package/src/core/handoff-context.ts +0 -136
- package/src/core/handoff-tools.ts +0 -133
- package/src/core/menu.ts +0 -70
- package/src/core/plan-tasks.ts +0 -43
- package/src/core/reminder.ts +0 -124
- package/src/core/sdd.ts +0 -353
- package/src/state.ts +0 -22
- package/templates/superpowers-doc-contract.md +0 -75
- package/vendor/superpowers/skills/brainstorming/SKILL.md +0 -159
- package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +0 -213
- package/vendor/superpowers/skills/brainstorming/scripts/helper.js +0 -167
- package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +0 -723
- package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +0 -209
- package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +0 -120
- package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +0 -49
- package/vendor/superpowers/skills/brainstorming/visual-companion.md +0 -291
- package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +0 -185
- package/vendor/superpowers/skills/executing-plans/SKILL.md +0 -70
- package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +0 -241
- package/vendor/superpowers/skills/receiving-code-review/SKILL.md +0 -213
- package/vendor/superpowers/skills/requesting-code-review/SKILL.md +0 -103
- package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +0 -172
- package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +0 -426
- package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +0 -139
- package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +0 -44
- package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +0 -22
- package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +0 -40
- package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +0 -188
- package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +0 -119
- package/vendor/superpowers/skills/systematic-debugging/SKILL.md +0 -296
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +0 -115
- package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +0 -122
- package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +0 -63
- package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +0 -169
- package/vendor/superpowers/skills/systematic-debugging/test-academic.md +0 -14
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +0 -58
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +0 -68
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +0 -69
- package/vendor/superpowers/skills/test-driven-development/SKILL.md +0 -371
- package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +0 -299
- package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +0 -202
- package/vendor/superpowers/skills/using-superpowers/SKILL.md +0 -62
- package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +0 -23
- package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +0 -39
- package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +0 -16
- package/vendor/superpowers/skills/verification-before-completion/SKILL.md +0 -139
- package/vendor/superpowers/skills/writing-plans/SKILL.md +0 -174
- package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
- package/vendor/superpowers/skills/writing-skills/SKILL.md +0 -689
- package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +0 -1150
- package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +0 -189
- package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +0 -172
- package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +0 -187
- package/vendor/superpowers/skills/writing-skills/render-graphs.js +0 -168
- 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
|
+
}
|
package/src/core/pr-create.ts
CHANGED
|
@@ -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
|
-
|
|
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" };
|
package/src/core/registration.ts
CHANGED
|
@@ -161,22 +161,42 @@ export function mergeCursorMcp(
|
|
|
161
161
|
return { config: base, changed };
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/** Swap the sessionStart hook command
|
|
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)
|
|
177
|
-
|
|
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
|
|
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
|