@workit-dev/core 0.0.1 → 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 (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -43
  3. package/package.json +56 -6
  4. package/schemas/config.json +74 -0
  5. package/schemas/vcs.json +79 -0
  6. package/schemas/workspaces.json +1029 -0
  7. package/schemas/youtrack.json +84 -0
  8. package/scripts/analyze-release-scope.ts +215 -0
  9. package/scripts/bundled-deps.json +131 -0
  10. package/scripts/fixtures/sample-plan.md +15 -0
  11. package/scripts/fixtures/sample-sdd/progress.md +2 -0
  12. package/scripts/install-codex-plugin.sh +28 -0
  13. package/scripts/install-cursor-plugin.sh +146 -0
  14. package/scripts/install-opencode-plugin.sh +79 -0
  15. package/scripts/install-pi-package.sh +56 -0
  16. package/scripts/lib/config-dir.sh +28 -0
  17. package/scripts/publish-changed-packages.ts +395 -0
  18. package/scripts/rewrite-workspace-deps.ts +53 -0
  19. package/scripts/run-cursor-mcp.sh +6 -0
  20. package/scripts/sync-release-manifests.ts +82 -0
  21. package/scripts/sync-runtime.sh +142 -0
  22. package/scripts/validate-cursor-marketplace.ts +171 -0
  23. package/scripts/verify-bundle-sources.ts +134 -0
  24. package/scripts/write-schemas.ts +16 -0
  25. package/skills/workit-architecture/SKILL.md +72 -0
  26. package/skills/workit-architecture/agents/openai.yaml +3 -0
  27. package/skills/workit-architecture/references/instruction-files.md +52 -0
  28. package/skills/workit-architecture/references/vocabulary.md +53 -0
  29. package/skills/workit-bdd/SKILL.md +53 -0
  30. package/skills/workit-continue/SKILL.md +54 -0
  31. package/skills/workit-debug/SKILL.md +45 -0
  32. package/skills/workit-deslop/SKILL.md +58 -0
  33. package/skills/workit-fanout/SKILL.md +71 -0
  34. package/skills/workit-fanout/references/brief.md +141 -0
  35. package/skills/workit-fanout/references/orchestration.md +169 -0
  36. package/skills/workit-implement/SKILL.md +71 -0
  37. package/skills/workit-retro/SKILL.md +72 -0
  38. package/skills/workit-retro/agents/openai.yaml +3 -0
  39. package/skills/workit-retro/references/sources.md +35 -0
  40. package/skills/workit-retro/references/steering.md +39 -0
  41. package/skills/workit-review/SKILL.md +66 -0
  42. package/skills/workit-review/references/impact.md +24 -0
  43. package/skills/workit-shape/SKILL.md +71 -0
  44. package/skills/workit-shape/references/diagrams.md +17 -0
  45. package/skills/workit-shape/references/knowledge.md +61 -0
  46. package/skills/workit-shape/references/mockups.md +15 -0
  47. package/skills/workit-shape/references/slicing.md +44 -0
  48. package/skills/workit-ship/SKILL.md +71 -0
  49. package/skills/workit-ship/references/changelog.md +44 -0
  50. package/skills/workit-ship/references/pr-body.md +32 -0
  51. package/skills/workit-ship/references/release.md +39 -0
  52. package/skills/workit-test-audit/SKILL.md +54 -0
  53. package/skills/workit-verify-app/SKILL.md +68 -0
  54. package/skills/workit-verify-app/references/template.md +51 -0
  55. package/src/autonomy.ts +910 -0
  56. package/src/check-config.ts +613 -0
  57. package/src/checks.ts +534 -0
  58. package/src/ci-waiter.ts +159 -0
  59. package/src/core/boundary.ts +55 -0
  60. package/src/core/branch-policy.ts +112 -0
  61. package/src/core/branch.ts +1192 -0
  62. package/src/core/changelog-gate.ts +349 -0
  63. package/src/core/changelog-merge.ts +214 -0
  64. package/src/core/changelog.ts +392 -0
  65. package/src/core/cleanup.ts +638 -0
  66. package/src/core/cli-locate.ts +173 -0
  67. package/src/core/commit-flavors.ts +65 -0
  68. package/src/core/config-schemas.ts +653 -0
  69. package/src/core/config.ts +351 -0
  70. package/src/core/context-read.ts +144 -0
  71. package/src/core/docs-gate.ts +69 -0
  72. package/src/core/finding-evidence.ts +89 -0
  73. package/src/core/finding-queue.ts +36 -0
  74. package/src/core/git.ts +73 -0
  75. package/src/core/gitignore.ts +52 -0
  76. package/src/core/heavy-eta.ts +170 -0
  77. package/src/core/heavy-lease.ts +290 -0
  78. package/src/core/heavy-rules.ts +373 -0
  79. package/src/core/heavy-scope.ts +131 -0
  80. package/src/core/heavy-words.ts +83 -0
  81. package/src/core/heavy.ts +535 -0
  82. package/src/core/hygiene.ts +149 -0
  83. package/src/core/init.ts +218 -0
  84. package/src/core/live-session.ts +333 -0
  85. package/src/core/logger.ts +310 -0
  86. package/src/core/methods.ts +185 -0
  87. package/src/core/model-fallback.ts +208 -0
  88. package/src/core/node-resolve.mjs +249 -0
  89. package/src/core/operation-input.ts +632 -0
  90. package/src/core/package-root.ts +28 -0
  91. package/src/core/policy/attributes.ts +97 -0
  92. package/src/core/policy/bookkeeping.ts +119 -0
  93. package/src/core/policy/carry.ts +122 -0
  94. package/src/core/policy/check-evidence.ts +115 -0
  95. package/src/core/policy/current-judgment.ts +20 -0
  96. package/src/core/policy/derive.ts +387 -0
  97. package/src/core/policy/judgment.ts +246 -0
  98. package/src/core/policy/lane.ts +562 -0
  99. package/src/core/policy/pr-lane.ts +32 -0
  100. package/src/core/policy/substance.ts +365 -0
  101. package/src/core/policy-resolver.ts +94 -0
  102. package/src/core/pr-create.ts +448 -0
  103. package/src/core/pr-template.ts +162 -0
  104. package/src/core/release-notes.ts +197 -0
  105. package/src/core/release-tracks.ts +779 -0
  106. package/src/core/remotes.ts +436 -0
  107. package/src/core/repo-context.ts +542 -0
  108. package/src/core/repo-paths.ts +29 -0
  109. package/src/core/resources.ts +1299 -0
  110. package/src/core/role-models.ts +603 -0
  111. package/src/core/route-intent.ts +305 -0
  112. package/src/core/rules.ts +65 -0
  113. package/src/core/runtime-identity.ts +63 -0
  114. package/src/core/safe-write.ts +87 -0
  115. package/src/core/scope-glob.ts +56 -0
  116. package/src/core/scripts.ts +6 -0
  117. package/src/core/skill-manifests.ts +173 -0
  118. package/src/core/store-lock.ts +374 -0
  119. package/src/core/support-matrix.ts +24 -0
  120. package/src/core/task-context.ts +247 -0
  121. package/src/core/task-contract.ts +1279 -0
  122. package/src/core/task-delivery.ts +109 -0
  123. package/src/core/task-engine.ts +2248 -0
  124. package/src/core/task-evaluation.ts +1147 -0
  125. package/src/core/task-store.ts +2649 -0
  126. package/src/core/task-view.ts +180 -0
  127. package/src/core/templates.ts +22 -0
  128. package/src/core/tracker-issues.ts +174 -0
  129. package/src/core/vcs-config.ts +540 -0
  130. package/src/core/workers.ts +62 -0
  131. package/src/core/workit-paths.mjs +242 -0
  132. package/src/core/workspace-branch-policy.ts +123 -0
  133. package/src/core/workspaces.ts +1745 -0
  134. package/src/core/youtrack-backend.ts +288 -0
  135. package/src/core/youtrack-tools.ts +260 -0
  136. package/src/core/youtrack-writes.ts +73 -0
  137. package/src/core/youtrack-ytrack.ts +217 -0
  138. package/src/core/youtrack.ts +454 -0
  139. package/src/core/ytrack-skill.ts +297 -0
  140. package/src/core.ts +171 -0
  141. package/src/deslop/lex.ts +271 -0
  142. package/src/deslop/scan.ts +232 -0
  143. package/src/duration.ts +27 -0
  144. package/src/fanout-brief.ts +486 -0
  145. package/src/fanout-check.ts +455 -0
  146. package/src/fanout-landed.ts +370 -0
  147. package/src/fanout-stack.ts +108 -0
  148. package/src/fanout-status.ts +362 -0
  149. package/src/fanout-worktree.ts +893 -0
  150. package/src/fanout.ts +1473 -0
  151. package/src/forge/exec.ts +385 -0
  152. package/src/forge/github.ts +831 -0
  153. package/src/forge/gitlab.ts +906 -0
  154. package/src/forge/issue-forge.ts +209 -0
  155. package/src/forge/issue-link.ts +468 -0
  156. package/src/forge/issue-ops.ts +314 -0
  157. package/src/forge/merge-gate-base-red.ts +72 -0
  158. package/src/forge/pr-body.ts +100 -0
  159. package/src/forge/pr-edit.ts +292 -0
  160. package/src/forge/pr-ops.ts +1109 -0
  161. package/src/forge/pr-status.ts +10 -0
  162. package/src/forge/redact.ts +195 -0
  163. package/src/forge/report.ts +994 -0
  164. package/src/forge/reruns.ts +132 -0
  165. package/src/forge/resolve.ts +509 -0
  166. package/src/forge/types.ts +242 -0
  167. package/src/forge/verify.ts +398 -0
  168. package/src/git/default-base.ts +130 -0
  169. package/src/git/env-redirect.ts +28 -0
  170. package/src/git/facts.ts +122 -0
  171. package/src/git/ops.ts +1823 -0
  172. package/src/git/refs-read.ts +332 -0
  173. package/src/git/release.ts +1020 -0
  174. package/src/git/rev.ts +1175 -0
  175. package/src/git/session-hook.ts +597 -0
  176. package/src/git/stage.ts +63 -0
  177. package/src/hooks/agent-model.ts +149 -0
  178. package/src/hooks/budget-signal.ts +284 -0
  179. package/src/hooks/context.ts +461 -0
  180. package/src/hooks/cwd-normalize.ts +79 -0
  181. package/src/hooks/deadline.ts +228 -0
  182. package/src/hooks/delivery-verbs.ts +182 -0
  183. package/src/hooks/descriptor.ts +124 -0
  184. package/src/hooks/handle.ts +433 -0
  185. package/src/hooks/heavy-gate.ts +99 -0
  186. package/src/hooks/hook-runs.ts +130 -0
  187. package/src/hooks/hosts/claude-code.ts +380 -0
  188. package/src/hooks/hosts/codex.ts +372 -0
  189. package/src/hooks/hosts/cursor.ts +419 -0
  190. package/src/hooks/hosts/fields.ts +98 -0
  191. package/src/hooks/hosts/opencode.ts +79 -0
  192. package/src/hooks/hosts/pi.ts +78 -0
  193. package/src/hooks/index.ts +60 -0
  194. package/src/hooks/model-limit.ts +378 -0
  195. package/src/hooks/model-switch.ts +35 -0
  196. package/src/hooks/policy.ts +29 -0
  197. package/src/hooks/process.ts +48 -0
  198. package/src/hooks/protocol.ts +135 -0
  199. package/src/hooks/raw-git.ts +2289 -0
  200. package/src/hooks/repeat-failure.ts +115 -0
  201. package/src/hooks/run.ts +54 -0
  202. package/src/hooks/session-marker.ts +151 -0
  203. package/src/hooks/shell-words.ts +532 -0
  204. package/src/hooks/skill-nudge.ts +295 -0
  205. package/src/hooks/spawn-gate.ts +72 -0
  206. package/src/hooks/standing-signal.ts +99 -0
  207. package/src/hooks/standing.ts +225 -0
  208. package/src/hooks/steering.ts +74 -0
  209. package/src/hooks/stop.ts +408 -0
  210. package/src/hooks/worktree-confine.ts +415 -0
  211. package/src/hooks/write-gate.ts +1174 -0
  212. package/src/host-session.ts +459 -0
  213. package/src/knowledge.ts +368 -0
  214. package/src/ledger.ts +2913 -0
  215. package/src/stack.ts +2570 -0
  216. package/src/store/event-log.ts +217 -0
  217. package/src/store/notes.ts +29 -0
  218. package/src/store/patch.ts +135 -0
  219. package/src/store/paths.ts +368 -0
  220. package/src/store/reduce.ts +203 -0
  221. package/src/test-audit-mocks.ts +309 -0
  222. package/src/verdict-cover.ts +151 -0
  223. package/src/worktree-deps.ts +142 -0
  224. package/templates/headers.md +3 -0
  225. package/templates/hygiene/.editorconfig +8 -0
  226. package/templates/hygiene/.gitattributes +3 -0
  227. package/templates/hygiene/CHANGELOG.md +14 -0
  228. package/templates/hygiene/CONTRIBUTING.md +3 -0
  229. package/templates/hygiene/LICENSE +21 -0
  230. package/templates/hygiene/README.md +3 -0
  231. package/templates/issue-update.md +5 -0
  232. package/templates/plan-template.md +26 -0
  233. package/templates/spec-template.md +64 -0
  234. package/templates/workit-contract.md +13 -0
@@ -0,0 +1,310 @@
1
+ import { appendFileSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { stateDir as workitStateDir } from "./workit-paths.mjs";
5
+
6
+ // Secret-safe structured logger (DG-01-DG-03, DG-05, DG-10). Host-neutral:
7
+ // node: builtins only. Never logs prompts, messages, content, raw tool
8
+ // arguments/results, credentials, tokens, authorization headers, issue data,
9
+ // URL queries, home prefixes, or unbounded stacks. Every record is bounded,
10
+ // redacted, rate-limited JSONL under a daily filename.
11
+
12
+ export type JsonValue =
13
+ | null
14
+ | boolean
15
+ | number
16
+ | string
17
+ | JsonValue[]
18
+ | { [key: string]: JsonValue };
19
+
20
+ export type LogLevel = "debug" | "info" | "warn" | "error";
21
+
22
+ export type LogEvent = {
23
+ level: LogLevel;
24
+ time: string;
25
+ message: string;
26
+ context: Record<string, JsonValue>;
27
+ };
28
+
29
+ export type LogSink = (event: LogEvent) => void;
30
+
31
+ export type LoggerOptions = {
32
+ stateDir?: string;
33
+ now?: () => Date;
34
+ appLog?: LogSink;
35
+ stderr?: LogSink;
36
+ maxRate?: number;
37
+ rateWindowMs?: number;
38
+ maxFieldLength?: number;
39
+ maxStackLines?: number;
40
+ };
41
+
42
+ export type Logger = {
43
+ debug: (message: string, context?: Record<string, unknown>) => void;
44
+ info: (message: string, context?: Record<string, unknown>) => void;
45
+ warn: (message: string, context?: Record<string, unknown>) => void;
46
+ error: (message: string, context?: Record<string, unknown>) => void;
47
+ guard: <T>(name: string, fn: () => T) => T | undefined;
48
+ };
49
+
50
+ export const REDACTED = "[REDACTED]";
51
+
52
+ const DEFAULT_MAX_RATE = 20;
53
+ const DEFAULT_RATE_WINDOW_MS = 1000;
54
+ const DEFAULT_MAX_FIELD_LENGTH = 200;
55
+ const DEFAULT_MAX_STACK_LINES = 30;
56
+
57
+ const DAY_FILE = /^workit-\d{4}-\d{2}-\d{2}\.jsonl$/;
58
+ const RETAINED_DAYS = 7;
59
+
60
+ // Home prefixes, URL queries, and inline secret values are redacted inside any
61
+ // string. Key names drive the rest: a field whose name contains a secret or
62
+ // content word is fully replaced, a stack/trace field is line-bounded.
63
+ const SECRET_VALUE = /\b(?:Bearer|Basic|Digest|Token)\s+\S+/gi;
64
+ // Case-insensitive: `Authorization: abc`, `Api-Token=xyz`, `Bearer`-style
65
+ // headers are redacted regardless of casing (D2). The alternation is the same
66
+ // secret vocabulary as SENSITIVE_WORDS, so the over-redaction risk is limited
67
+ // to values next to a secret-ish key — acceptable for a security logger.
68
+ const KEY_EQ_VALUE =
69
+ /\b([A-Za-z0-9_-]*(?:token|secret|password|passwd|apikey|api[_-]?key|authorization|credential|bearer)[A-Za-z0-9_-]*)([:=]\s*).+/gi;
70
+ const URL_QUERY = /(https?:\/\/[^?#\s]+)\?[^#\s]*/g;
71
+
72
+ // Split camelCase AND acronym+word compounds (APIToken -> api|token, so the
73
+ // "token" word lands in SENSITIVE_WORDS instead of the whole lowercase
74
+ // compound "apitoken" leaking past it) (D1).
75
+ const splitKey = (key: string): string[] =>
76
+ key
77
+ .split(/[_-]+|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/)
78
+ .map((word) => word.toLowerCase());
79
+
80
+ const SENSITIVE_WORDS = new Set([
81
+ "token",
82
+ "secret",
83
+ "password",
84
+ "passwd",
85
+ "authorization",
86
+ "credential",
87
+ "credentials",
88
+ "cookie",
89
+ "api",
90
+ "apikey",
91
+ "apitoken",
92
+ "accesstoken",
93
+ "clientsecret",
94
+ "refreshtoken",
95
+ "key",
96
+ "bearer",
97
+ "prompt",
98
+ "message",
99
+ "messages",
100
+ "content",
101
+ "body",
102
+ "args",
103
+ "argument",
104
+ "result",
105
+ "results",
106
+ "output",
107
+ "issue",
108
+ "description",
109
+ "summary",
110
+ "payload",
111
+ "script",
112
+ "command",
113
+ "text",
114
+ "query",
115
+ ]);
116
+
117
+ const STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
118
+
119
+ const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
120
+
121
+ /** Workit's state dir for this process (core/workit-paths.mjs). */
122
+ export const resolveStateDir = (): string => workitStateDir(process.env);
123
+
124
+ type RedactOptions = {
125
+ maxFieldLength: number;
126
+ maxStackLines: number;
127
+ };
128
+
129
+ // The separator lookahead accepts both / and \ so home-relative Windows paths
130
+ // (C:\Users\x\repo) redact too — os.homedir() uses backslashes there.
131
+ const homePattern = new RegExp(
132
+ `${escapeRegExp(os.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`,
133
+ "g",
134
+ );
135
+
136
+ const applyPatterns = (value: string): string => {
137
+ let out = value;
138
+ out = out.replace(SECRET_VALUE, REDACTED);
139
+ out = out.replace(KEY_EQ_VALUE, "$1$2[REDACTED]");
140
+ out = out.replace(URL_QUERY, "$1?[REDACTED]");
141
+ out = out.replace(homePattern, "~");
142
+ // win32 paths keep backslashes after the ~ substitution (~\repo); normalize
143
+ // separators so logged paths are portable and match the shell-shaped ~/
144
+ // form. Other platforms are untouched.
145
+ if (process.platform === "win32") out = out.replace(/\\/g, "/");
146
+ return out;
147
+ };
148
+
149
+ /**
150
+ * Pattern-only redaction: strips secret value patterns from a string while
151
+ * preserving its full text — no field-length truncation. The receiver owns its
152
+ * own size bound (e.g. an MCP error string must stay intact for the client).
153
+ */
154
+ export const redactSecrets = (value: string): string => applyPatterns(value);
155
+
156
+ const redactString = (value: string, options: RedactOptions): string => {
157
+ const out = applyPatterns(value);
158
+ if (out.length > options.maxFieldLength) {
159
+ return `${out.slice(0, options.maxFieldLength)}…`;
160
+ }
161
+ return out;
162
+ };
163
+
164
+ const boundStack = (value: unknown, options: RedactOptions): JsonValue => {
165
+ if (typeof value !== "string") return redact(value, options);
166
+ const lines = value.split("\n");
167
+ const kept = lines.slice(0, options.maxStackLines).map(applyPatterns);
168
+ const dropped = lines.length - kept.length;
169
+ const body = dropped > 0 ? [...kept, ` ... ${dropped} more`] : kept;
170
+ return body.join("\n");
171
+ };
172
+
173
+ export const redact = (value: unknown, options: RedactOptions = DEFAULT_OPTIONS): JsonValue => {
174
+ if (value === null || value === undefined) return null;
175
+ if (typeof value === "boolean" || typeof value === "number") return value;
176
+ if (typeof value === "string") return redactString(value, options);
177
+ if (value instanceof Date) return value.toISOString();
178
+ if (Array.isArray(value)) return value.map((entry) => redact(entry, options));
179
+ if (typeof value === "object") {
180
+ const out: Record<string, JsonValue> = {};
181
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
182
+ const words = splitKey(key);
183
+ if (words.some((word) => STACK_WORDS.has(word))) out[key] = boundStack(entry, options);
184
+ else if (words.some((word) => SENSITIVE_WORDS.has(word))) out[key] = REDACTED;
185
+ else out[key] = redact(entry, options);
186
+ }
187
+ return out;
188
+ }
189
+ return null;
190
+ };
191
+
192
+ const DEFAULT_OPTIONS: RedactOptions = {
193
+ maxFieldLength: DEFAULT_MAX_FIELD_LENGTH,
194
+ maxStackLines: DEFAULT_MAX_STACK_LINES,
195
+ };
196
+
197
+ const dailyFileName = (date: Date): string => {
198
+ const y = date.getFullYear();
199
+ const m = String(date.getMonth() + 1).padStart(2, "0");
200
+ const d = String(date.getDate()).padStart(2, "0");
201
+ return `workit-${y}-${m}-${d}.jsonl`;
202
+ };
203
+
204
+ const pruneOldFiles = (dir: string): void => {
205
+ try {
206
+ const files = readdirSync(dir)
207
+ .filter((name) => DAY_FILE.test(name))
208
+ .toSorted()
209
+ .toReversed();
210
+ for (const file of files.slice(RETAINED_DAYS)) {
211
+ try {
212
+ unlinkSync(path.join(dir, file));
213
+ } catch {
214
+ // a concurrent prune may already have removed it
215
+ }
216
+ }
217
+ } catch {
218
+ // logging must never break the host
219
+ }
220
+ };
221
+
222
+ export const createLogger = (options: LoggerOptions = {}): Logger => {
223
+ const stateDir = options.stateDir ?? resolveStateDir();
224
+ const logDir = path.join(stateDir, "logs");
225
+ const now = options.now ?? (() => new Date());
226
+ const maxRate = options.maxRate ?? DEFAULT_MAX_RATE;
227
+ const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
228
+ const redactOptions: RedactOptions = {
229
+ maxFieldLength: options.maxFieldLength ?? DEFAULT_MAX_FIELD_LENGTH,
230
+ maxStackLines: options.maxStackLines ?? DEFAULT_MAX_STACK_LINES,
231
+ };
232
+
233
+ let windowStart = 0;
234
+ const windowCounts: Record<LogLevel, number> = { debug: 0, info: 0, warn: 0, error: 0 };
235
+
236
+ const emit = (level: LogLevel, message: string, context?: Record<string, unknown>): void => {
237
+ const date = now();
238
+ const elapsed = date.getTime() - windowStart;
239
+ if (elapsed >= rateWindowMs || windowStart === 0) {
240
+ windowStart = date.getTime();
241
+ windowCounts.debug = 0;
242
+ windowCounts.info = 0;
243
+ windowCounts.warn = 0;
244
+ windowCounts.error = 0;
245
+ }
246
+ windowCounts[level] += 1;
247
+ if (windowCounts[level] > maxRate) return;
248
+
249
+ // ponytail: per-level budget so an info flood can't starve warn/error
250
+ // canaries; shared-token budget would need cross-level prioritization.
251
+ let event: LogEvent | null = null;
252
+ try {
253
+ event = {
254
+ level,
255
+ time: date.toISOString(),
256
+ message: redactString(message, redactOptions),
257
+ context: redact(context ?? {}, redactOptions) as Record<string, JsonValue>,
258
+ };
259
+ const line = `${JSON.stringify(event)}\n`;
260
+ mkdirSync(logDir, { recursive: true, mode: 0o700 });
261
+ appendFileSync(path.join(logDir, dailyFileName(date)), line, { mode: 0o600 });
262
+ pruneOldFiles(logDir);
263
+ } catch {
264
+ // Serialization (e.g. a circular context) must never throw into the
265
+ // caller: record a bounded failure event instead.
266
+ try {
267
+ event = {
268
+ level,
269
+ time: date.toISOString(),
270
+ message: redactString(message, redactOptions),
271
+ context: { redaction_failed: true },
272
+ };
273
+ mkdirSync(logDir, { recursive: true, mode: 0o700 });
274
+ appendFileSync(path.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}\n`, {
275
+ mode: 0o600,
276
+ });
277
+ } catch {
278
+ // logging must never break the host
279
+ }
280
+ }
281
+
282
+ for (const sink of [options.appLog, options.stderr]) {
283
+ if (!sink) continue;
284
+ if (!event) continue;
285
+ try {
286
+ sink(event);
287
+ } catch {
288
+ // a broken sink must not break the caller
289
+ }
290
+ }
291
+ };
292
+
293
+ return {
294
+ debug: (message, context) => emit("debug", message, context),
295
+ info: (message, context) => emit("info", message, context),
296
+ warn: (message, context) => emit("warn", message, context),
297
+ error: (message, context) => emit("error", message, context),
298
+ guard: <T>(name: string, fn: () => T): T | undefined => {
299
+ try {
300
+ return fn();
301
+ } catch (err) {
302
+ emit("warn", "detector_failed", {
303
+ detector: name,
304
+ error: err instanceof Error ? err.message : String(err),
305
+ });
306
+ return undefined;
307
+ }
308
+ },
309
+ };
310
+ };
@@ -0,0 +1,185 @@
1
+ import { claudeSpawnTable } from "./role-models";
2
+ import type { Assurance, Capability, Dimension, Policy } from "./task-contract";
3
+ import { hostFamily } from "./workspaces";
4
+
5
+ export type MethodId =
6
+ | "workit-shape"
7
+ | "workit-bdd"
8
+ | "workit-review"
9
+ | "workit-implement"
10
+ | "workit-fanout"
11
+ | "workit-debug"
12
+ | "workit-continue"
13
+ | "workit-deslop";
14
+
15
+ type MethodDefinition = {
16
+ dimensions?: readonly Dimension[];
17
+ ruleIds?: readonly string[];
18
+ };
19
+
20
+ export const METHODS: Readonly<Record<MethodId, MethodDefinition>> = {
21
+ "workit-shape": {
22
+ dimensions: ["challenge", "decisions", "artifacts", "continuity"],
23
+ },
24
+ "workit-bdd": { dimensions: ["testing"] },
25
+ "workit-review": {
26
+ dimensions: ["review"],
27
+ ruleIds: ["fresh-context-review", "self-review"],
28
+ },
29
+ // Mechanical work needs only the existing checks, not a RED/GREEN ritual.
30
+ "workit-implement": { ruleIds: ["mechanical-existing-checks"] },
31
+ "workit-fanout": { dimensions: ["delegation"] },
32
+ // workit-debug and workit-continue stay trigger- and slash-invoked: no
33
+ // generated rule routes to them, so they never appear spuriously.
34
+ "workit-debug": {},
35
+ "workit-continue": {},
36
+ "workit-deslop": { ruleIds: ["pre-pr-cleanup"] },
37
+ };
38
+
39
+ export type SelectedMethod = {
40
+ id: MethodId;
41
+ reason: string;
42
+ assurance: Assurance;
43
+ };
44
+
45
+ const methodMatches = (
46
+ definition: MethodDefinition,
47
+ requirement: Policy["requirements"][number],
48
+ ): boolean =>
49
+ definition.dimensions?.includes(requirement.dimension) === true ||
50
+ definition.ruleIds?.includes(requirement.ruleId) === true;
51
+
52
+ const assuranceFor = (
53
+ id: MethodId,
54
+ definition: MethodDefinition,
55
+ capabilities: Capability[],
56
+ ): { assurance: Assurance; reasons: string[] } => {
57
+ const keys = new Set<string>([
58
+ id,
59
+ ...(definition.dimensions ?? []),
60
+ ...(definition.ruleIds ?? []),
61
+ ]);
62
+ const matches = capabilities.filter(
63
+ (capability) => keys.has(capability.name) || keys.has(capability.surface),
64
+ );
65
+ if (id === "workit-review" && matches.length === 0)
66
+ return {
67
+ assurance: "unavailable",
68
+ reasons: ["independent review capability is unavailable"],
69
+ };
70
+ if (matches.some((capability) => capability.assurance === "unavailable"))
71
+ return {
72
+ assurance: "unavailable",
73
+ reasons: matches
74
+ .filter((capability) => capability.assurance === "unavailable")
75
+ .map((capability) => capability.reason),
76
+ };
77
+ if (matches.some((capability) => capability.assurance === "enforced"))
78
+ return {
79
+ assurance: "enforced",
80
+ reasons: matches.map((capability) => capability.reason),
81
+ };
82
+ return {
83
+ assurance: "agent_guided",
84
+ reasons: matches.map((capability) => capability.reason),
85
+ };
86
+ };
87
+
88
+ export function selectMethods(policy: Policy, capabilities: Capability[]): SelectedMethod[] {
89
+ return (Object.entries(METHODS) as [MethodId, MethodDefinition][]).flatMap(([id, definition]) => {
90
+ const requirements = policy.requirements.filter((requirement) =>
91
+ methodMatches(definition, requirement),
92
+ );
93
+ if (requirements.length === 0) return [];
94
+ const capability = assuranceFor(id, definition, capabilities);
95
+ const reasons = [
96
+ ...new Set(requirements.map((requirement) => requirement.reason)),
97
+ ...capability.reasons,
98
+ ];
99
+ if (capability.assurance === "unavailable")
100
+ reasons.unshift("required capability is unavailable");
101
+ return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
102
+ });
103
+ }
104
+
105
+ const BOOTSTRAP = `
106
+ Workit is optional coordination and proof tooling around the host: you decide
107
+ what and whether, the CLI does how and records what it observed.
108
+ Native host allow/ask/deny, sandbox and org rules stay authoritative. Never
109
+ switch paths to evade a denial, never claim enforcement a host cannot provide,
110
+ and never fabricate an approval or verdict. A question answer is not
111
+ host permission, and imported decisions or handoffs grant no authority.
112
+
113
+ Autonomy contract:
114
+ - Continue to the requested endpoint (commit, push, PR, green CI, verified,
115
+ merged) without asking "continue?", running checks and repairs.
116
+ Stop only for a new consequential choice, a host denial, a conflicting edit
117
+ or an unresolved blocker; report open gaps and uncertain workers.
118
+ - No endpoint named: stop at a local commit on a policy-compliant branch and
119
+ state the next command. If the request implies delivery (fix, implement,
120
+ ship), go to the effective endpoint in \`workit grant show\`: \`pr\` stops
121
+ at the PR even with merge granted; \`green\` babysits it (workit-ship) to
122
+ merge-ready, never merging; \`merged\` lands it with \`workit pr merge\`.
123
+ Only the user raises a grant or the endpoint, in a terminal outside the agent
124
+ host (\`workit grant set <ws> defaultEndpoint=merged\`); never edit
125
+ workspaces.json or route around grant_required. Release: \`workit release
126
+ start <v>\`, commits, \`finish\`; \`publish\` if asked.
127
+ - Ask only for a product or preference choice, or for authority you lack, with
128
+ your recommended answer. Facts are yours: read, run or prototype.
129
+ - Label claims measured (run and seen this session), inferred, or guess.
130
+ Prose is a note: only a configured check the CLI ran (\`workit check <name>\`)
131
+ satisfies a gate; an ad-hoc \`workit check -- <cmd>\` never does.
132
+ - Author is not verifier: your own verdict is \`--self\`, shown as
133
+ self-reviewed, never verified. High risk, a focused or full lane and
134
+ merge under \`verification: self|independent\` need a verifier with its
135
+ own host-derived session (a subagent's \`<lead>:<child>\`); never hand-mint one (\`<lead>-v1\`).
136
+ - A local-commit endpoint does not imply PR readiness, and a local commit alone
137
+ is not evidence of a requested remote push. Before reporting delivery,
138
+ reconcile every requested item and observe it (\`workit verify-delivery\`).
139
+
140
+ Read-only git and gh run raw; mutations go through workit verbs
141
+ (\`workit git|pr|ci|check|help\`; else \`npx -y @workit-dev/cli\`).
142
+ Other checkout: \`workit <verb> --cwd <dir> …\` (verb first: host rules match it); one workit
143
+ verb per shell call, never \`cd <dir> && workit\`. zsh-safe: no \`echo =====\`
144
+ (\`printf '%s\\n' ---\`), quote globs and URLs, no variable named \`status\` or \`path\`.
145
+ Add Co-Authored-By via \`workit git commit --trailer "K: v" -- <paths>\`, never
146
+ an amend.
147
+ The task families (task, policy, evidence, finding, decision, worker, state)
148
+ are optional continuity for tracked work; each branch has one implicit task,
149
+ and a solo edit needs no task. A decision records a user choice; it never
150
+ authorizes an effect. Judge work once with
151
+ \`workit policy assess --judge risk= behavior= product-choice= plan=\`.
152
+ Across repositories, bind items to checkout, branch and endpoint, resolve
153
+ competing targets before a mutation, and reconcile an uncertain
154
+ effect before a retry.
155
+
156
+ Mid-task: never silently drop or resume an objective (workit-continue).
157
+
158
+ Skills: load the skill, never act from memory (aliases /wk-<name>).
159
+ - brainstorm, plan, spec, grill, should we: workit-shape
160
+ - implement, build, add a feature: workit-implement
161
+ - review, blast radius: workit-review
162
+ - bug, broken, flaky, regression: workit-debug
163
+ - ship, babysit, CI, merge: workit-ship
164
+ - resume, pick up, handoff, interruption: workit-continue
165
+ - BDD, TDD, acceptance criteria, Given/When/Then: workit-bdd
166
+ - test audit, tautology, weak tests: workit-test-audit
167
+ - deslop, slop, dead code: workit-deslop
168
+ - fan out, parallelize, parallel agents, swarm: workit-fanout
169
+ - verify the app, smoke test, prove it works: workit-verify-app
170
+ - retro (user-invoked; offer it): workit-retro
171
+ - architecture, deepen modules (user-invoked; offer it): workit-architecture
172
+ `.trim();
173
+
174
+ /**
175
+ * The session contract. With the session's `cwd` and `host`, a Claude Code
176
+ * lead also gets one line naming every agent type's spawn model and effort,
177
+ * so its first spawn passes them (retro 2026-11-07 R1).
178
+ */
179
+ export const invariantBootstrap = (
180
+ session: { cwd: string; host: string } | null = null,
181
+ ): string => {
182
+ const table =
183
+ session && hostFamily(session.host) === "claude_code" ? claudeSpawnTable(session.cwd) : null;
184
+ return table ? `${BOOTSTRAP}\n${table}` : BOOTSTRAP;
185
+ };