devworkspace 0.2.1 → 0.3.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 (78) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +4 -3
  3. package/dist/agents/claude/content.js +4 -3
  4. package/dist/agents/claude/content.js.map +1 -1
  5. package/dist/cli/args.d.ts +19 -0
  6. package/dist/cli/args.js +6 -0
  7. package/dist/cli/args.js.map +1 -1
  8. package/dist/cli/help.js +32 -9
  9. package/dist/cli/help.js.map +1 -1
  10. package/dist/cli/render.d.ts +9 -1
  11. package/dist/cli/render.js +89 -11
  12. package/dist/cli/render.js.map +1 -1
  13. package/dist/cli/run.js +54 -8
  14. package/dist/cli/run.js.map +1 -1
  15. package/dist/config/schemas.d.ts +5 -0
  16. package/dist/config/schemas.js +2 -0
  17. package/dist/config/schemas.js.map +1 -1
  18. package/dist/discovery/cache.d.ts +19 -0
  19. package/dist/discovery/cache.js +50 -0
  20. package/dist/discovery/cache.js.map +1 -0
  21. package/dist/discovery/commands.js +17 -3
  22. package/dist/discovery/commands.js.map +1 -1
  23. package/dist/discovery/detect.d.ts +6 -0
  24. package/dist/discovery/detect.js +12 -6
  25. package/dist/discovery/detect.js.map +1 -1
  26. package/dist/discovery/docs.d.ts +10 -0
  27. package/dist/discovery/docs.js +52 -0
  28. package/dist/discovery/docs.js.map +1 -0
  29. package/dist/discovery/entrypoints.d.ts +20 -0
  30. package/dist/discovery/entrypoints.js +67 -0
  31. package/dist/discovery/entrypoints.js.map +1 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/integrations/adapters/claudePlugin.js +12 -3
  36. package/dist/integrations/adapters/claudePlugin.js.map +1 -1
  37. package/dist/knowledge/facts.d.ts +59 -2
  38. package/dist/knowledge/facts.js +115 -17
  39. package/dist/knowledge/facts.js.map +1 -1
  40. package/dist/prompts/onboarding.js +6 -3
  41. package/dist/prompts/onboarding.js.map +1 -1
  42. package/dist/services/analysis.d.ts +7 -1
  43. package/dist/services/analysis.js +17 -4
  44. package/dist/services/analysis.js.map +1 -1
  45. package/dist/services/doctor.js +14 -3
  46. package/dist/services/doctor.js.map +1 -1
  47. package/dist/services/knowledge.d.ts +17 -0
  48. package/dist/services/knowledge.js +28 -2
  49. package/dist/services/knowledge.js.map +1 -1
  50. package/dist/services/refresh.js +6 -6
  51. package/dist/services/refresh.js.map +1 -1
  52. package/dist/services/status.d.ts +11 -2
  53. package/dist/services/status.js +11 -9
  54. package/dist/services/status.js.map +1 -1
  55. package/dist/services/taskContext.d.ts +59 -0
  56. package/dist/services/taskContext.js +164 -0
  57. package/dist/services/taskContext.js.map +1 -0
  58. package/dist/services/tasks.d.ts +2 -0
  59. package/dist/services/tasks.js +20 -12
  60. package/dist/services/tasks.js.map +1 -1
  61. package/dist/services/verify.d.ts +47 -0
  62. package/dist/services/verify.js +172 -0
  63. package/dist/services/verify.js.map +1 -0
  64. package/dist/tasks/routing.js +10 -3
  65. package/dist/tasks/routing.js.map +1 -1
  66. package/dist/tasks/task.d.ts +49 -5
  67. package/dist/tasks/task.js +68 -11
  68. package/dist/tasks/task.js.map +1 -1
  69. package/dist/verification/safety.d.ts +38 -0
  70. package/dist/verification/safety.js +120 -0
  71. package/dist/verification/safety.js.map +1 -0
  72. package/dist/verification/scope.d.ts +19 -0
  73. package/dist/verification/scope.js +119 -0
  74. package/dist/verification/scope.js.map +1 -0
  75. package/docs/benchmarking.md +19 -0
  76. package/docs/knowledge.md +19 -0
  77. package/docs/testing.md +25 -2
  78. package/package.json +1 -1
@@ -21,7 +21,7 @@ export const TASK_TYPES = [
21
21
  'MAINTENANCE',
22
22
  ];
23
23
  export const TASK_STATUSES = ['IDEA', 'SPECIFIED', 'PLANNED', 'IN_PROGRESS', 'TESTING', 'REVIEW', 'BLOCKED', 'FAILED', 'COMPLETED'];
24
- export const VERIFY_STATUSES = ['PENDING', 'PASS', 'FAIL', 'SKIPPED'];
24
+ export const VERIFY_STATUSES = ['PENDING', 'PASS', 'FAIL', 'SKIPPED', 'COULD_NOT_RUN'];
25
25
  const TRANSITIONS = {
26
26
  IDEA: ['SPECIFIED', 'PLANNED', 'IN_PROGRESS', 'BLOCKED', 'FAILED'],
27
27
  SPECIFIED: ['PLANNED', 'IN_PROGRESS', 'BLOCKED', 'IDEA'],
@@ -126,7 +126,22 @@ export function validateTask(v, file = 'task') {
126
126
  ic.at_('check').str(io.check, { max: 50 });
127
127
  ic.at_('required').bool(io.required);
128
128
  ic.at_('status').oneOf(io.status, VERIFY_STATUSES);
129
+ if (io.evidence !== undefined) {
130
+ ic.at_('evidence')
131
+ .arr(io.evidence, 100)
132
+ .forEach((e, j) => {
133
+ const ec = c.at_(`verification.${i}.evidence.${j}`);
134
+ const eo = ec.obj(e);
135
+ ec.at_('kind').oneOf(eo.kind, ['executed', 'claimed']);
136
+ ec.at_('package').str(eo.package, { max: 240 });
137
+ });
138
+ }
129
139
  });
140
+ if (o.scope !== undefined) {
141
+ const sc = c.at_('scope').obj(o.scope);
142
+ c.at_('scope.capturedAt').iso(sc.capturedAt);
143
+ c.at_('scope.baseline').obj(sc.baseline);
144
+ }
130
145
  const a = c.at_('attestations').obj(o.attestations);
131
146
  c.at_('attestations.userWorkIntact').bool(a.userWorkIntact);
132
147
  c.at_('attestations.knowledgeReviewed').bool(a.knowledgeReviewed);
@@ -135,23 +150,65 @@ export function validateTask(v, file = 'task') {
135
150
  c.at_('history').arr(o.history, 500);
136
151
  return o;
137
152
  }
138
- /** The completion gate. A task is only COMPLETED when every applicable requirement holds. */
139
- export function evaluateGate(task) {
153
+ const UNKNOWN_SCOPE = 'unknown';
154
+ /** Per-package requirements of one check. Returns false when the contract has no applicable package entry. */
155
+ function packageGate(v, ctx, failures, warnings) {
156
+ const entries = ctx.contract.filter((c) => c.id === v.check && c.available && !c.mutates);
157
+ if (entries.length === 0)
158
+ return false;
159
+ const known = ctx.scope !== null;
160
+ const unknown = !known;
161
+ let applicable = 0;
162
+ for (const entry of entries) {
163
+ const pkg = entry.cwd ?? '.';
164
+ const fp = known ? ctx.scope[pkg] : UNKNOWN_SCOPE;
165
+ if (fp === undefined)
166
+ continue; // this package was not changed, so this check does not apply to it
167
+ applicable++;
168
+ const label = `${v.check} [${pkg}]`;
169
+ const ev = v.evidence?.find((e) => e.package === pkg);
170
+ if (!ev)
171
+ failures.push(`${label}: not run for the current changes`);
172
+ else if (ev.kind === 'claimed' && !ev.userOverride)
173
+ failures.push(`${label}: only claimed, not executed (run: devworkspace task run <id> ${v.check} --package ${pkg})`);
174
+ else if (ev.kind === 'claimed')
175
+ warnings.push(`${label}: accepted on the user's word; DevWorkspace did not observe it run`);
176
+ else if (ev.exitCode !== 0)
177
+ failures.push(`${label}: FAIL (exit ${ev.exitCode ?? 'none'})`);
178
+ else if (!unknown && ev.scope !== fp)
179
+ failures.push(`${label}: passed, but files changed afterwards; run it again`);
180
+ }
181
+ if (known && applicable === 0)
182
+ warnings.push(`${v.check}: no package with this check was changed`);
183
+ return true;
184
+ }
185
+ /**
186
+ * The completion gate. A task is only COMPLETED when every applicable requirement holds.
187
+ * Tasks that carry a `scope` (created by 0.2.2+) are checked per changed package against executed evidence; older
188
+ * tasks keep the original rules.
189
+ */
190
+ export function evaluateGate(task, ctx) {
140
191
  const failures = [];
141
192
  const warnings = [];
193
+ const scoped = !!ctx && !!task.scope;
142
194
  for (const v of task.verification) {
143
195
  if (!v.required)
144
196
  continue;
145
197
  if (v.status === 'FAIL')
146
198
  failures.push(`${v.check}: FAIL`);
147
- else if (v.status === 'PENDING')
148
- failures.push(`${v.check}: not run yet`);
199
+ else if (v.status === 'COULD_NOT_RUN')
200
+ failures.push(`${v.check}: could not run${v.detail ? ` (${v.detail})` : ''}`);
149
201
  else if (v.status === 'SKIPPED') {
150
202
  if (v.reason === 'user-override')
151
203
  warnings.push(`${v.check}: skipped at the user's request; completion is less certain`);
152
204
  else
153
205
  failures.push(`${v.check}: skipped without a user override`);
154
206
  }
207
+ else if (scoped && packageGate(v, ctx, failures, warnings)) {
208
+ // handled per package
209
+ }
210
+ else if (v.status === 'PENDING')
211
+ failures.push(`${v.check}: not run yet`);
155
212
  }
156
213
  const { baseline, after } = task.tests;
157
214
  if (baseline && !after)
@@ -175,7 +232,7 @@ export function evaluateGate(task) {
175
232
  }
176
233
  return { ok: failures.length === 0, failures, warnings };
177
234
  }
178
- export function transitionTask(task, to, now, note) {
235
+ export function transitionTask(task, to, now, note, ctx) {
179
236
  if (task.status === to)
180
237
  return null;
181
238
  if (!TRANSITIONS[task.status].includes(to)) {
@@ -185,7 +242,7 @@ export function transitionTask(task, to, now, note) {
185
242
  }
186
243
  let gate = null;
187
244
  if (to === 'COMPLETED') {
188
- gate = evaluateGate(task);
245
+ gate = evaluateGate(task, ctx);
189
246
  if (!gate.ok) {
190
247
  throw new DevWorkspaceError('GATE', `Task ${task.id} cannot be completed:\n - ${gate.failures.join('\n - ')}`, {
191
248
  hint: 'Resolve the items above (or mark the task BLOCKED/FAILED). A task is never complete while verification is failing.',
@@ -197,7 +254,7 @@ export function transitionTask(task, to, now, note) {
197
254
  task.updatedAt = now;
198
255
  return gate;
199
256
  }
200
- export function nextRequiredStep(task) {
257
+ export function nextRequiredStep(task, ctx) {
201
258
  switch (task.status) {
202
259
  case 'COMPLETED':
203
260
  return 'None. The task is complete.';
@@ -212,10 +269,10 @@ export function nextRequiredStep(task) {
212
269
  case 'PLANNED':
213
270
  return 'Start implementation (baseline verification first for meaningful changes).';
214
271
  default: {
215
- const pending = task.verification.filter((v) => v.required && (v.status === 'PENDING' || v.status === 'FAIL'));
272
+ const pending = task.verification.filter((v) => v.required && (v.status === 'PENDING' || v.status === 'FAIL' || v.status === 'COULD_NOT_RUN'));
216
273
  if (pending.length > 0)
217
- return `Run/fix verification: ${pending.map((p) => `${p.check} (${p.status.toLowerCase()})`).join(', ')}.`;
218
- const g = evaluateGate(task);
274
+ return `Run/fix verification: ${pending.map((p) => `${p.check} (${p.status.toLowerCase().replace(/_/g, ' ')})`).join(', ')}.`;
275
+ const g = evaluateGate(task, ctx);
219
276
  return g.ok ? 'All gates pass. Mark the task COMPLETED.' : `Satisfy the completion gate: ${g.failures[0]}`;
220
277
  }
221
278
  }
@@ -1 +1 @@
1
- {"version":3,"file":"task.js","sourceRoot":"","sources":["../../src/tasks/task.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport { Check } from '../config/validate.js';\nimport { SCHEMA_VERSION, type CommandEntry } from '../config/schemas.js';\nimport { atomicWriteJson, ensureDir, pathExists, randomId } from '../utils/fs.js';\nimport { readVersioned } from '../core/versioned.js';\nimport { backupIfVersionChanges, type Ws } from '../core/workspace.js';\nimport { containsSecret } from '../security/redact.js';\n\nexport const TASK_TYPES = [\n 'QUESTION',\n 'BUG',\n 'FEATURE',\n 'REFACTOR',\n 'DOCUMENTATION',\n 'TESTING',\n 'PERFORMANCE',\n 'SECURITY',\n 'ARCHITECTURE',\n 'RELEASE',\n 'MAINTENANCE',\n] as const;\nexport type TaskType = (typeof TASK_TYPES)[number];\n\nexport const TASK_STATUSES = ['IDEA', 'SPECIFIED', 'PLANNED', 'IN_PROGRESS', 'TESTING', 'REVIEW', 'BLOCKED', 'FAILED', 'COMPLETED'] as const;\nexport type TaskStatus = (typeof TASK_STATUSES)[number];\n\nexport const VERIFY_STATUSES = ['PENDING', 'PASS', 'FAIL', 'SKIPPED'] as const;\nexport type VerifyStatus = (typeof VERIFY_STATUSES)[number];\n\nexport interface VerificationItem {\n check: string;\n required: boolean;\n status: VerifyStatus;\n reason?: string;\n detail?: string;\n updatedAt?: string;\n}\n\nexport interface TestCounts {\n passed: number;\n failed: number;\n}\n\nexport interface Attestations {\n userWorkIntact: boolean;\n knowledgeReviewed: boolean;\n finalStateInspected: boolean;\n}\n\nexport interface Task {\n schemaVersion: number;\n id: string;\n title: string;\n type: TaskType;\n status: TaskStatus;\n createdAt: string;\n updatedAt: string;\n description: string;\n acceptanceCriteria: string[];\n plan: string[];\n affectedAreas: string[];\n verification: VerificationItem[];\n tests: { baseline?: TestCounts; after?: TestCounts };\n attestations: Attestations;\n decisions: string[];\n blockers: string[];\n constraints: string[];\n completionSummary?: string;\n history: { at: string; from: TaskStatus | null; to: TaskStatus; note?: string }[];\n}\n\nconst TRANSITIONS: Record<TaskStatus, TaskStatus[]> = {\n IDEA: ['SPECIFIED', 'PLANNED', 'IN_PROGRESS', 'BLOCKED', 'FAILED'],\n SPECIFIED: ['PLANNED', 'IN_PROGRESS', 'BLOCKED', 'IDEA'],\n PLANNED: ['IN_PROGRESS', 'SPECIFIED', 'BLOCKED', 'IDEA'],\n IN_PROGRESS: ['TESTING', 'REVIEW', 'COMPLETED', 'BLOCKED', 'FAILED', 'PLANNED'],\n TESTING: ['IN_PROGRESS', 'REVIEW', 'COMPLETED', 'BLOCKED', 'FAILED'],\n REVIEW: ['IN_PROGRESS', 'TESTING', 'COMPLETED', 'BLOCKED', 'FAILED'],\n BLOCKED: ['IN_PROGRESS', 'PLANNED', 'SPECIFIED', 'IDEA', 'FAILED'],\n FAILED: ['IN_PROGRESS', 'IDEA'],\n COMPLETED: ['IN_PROGRESS'],\n};\n\nexport function allowedTransitions(from: TaskStatus): TaskStatus[] {\n return TRANSITIONS[from];\n}\n\nconst CODE_CHANGING: TaskType[] = ['BUG', 'FEATURE', 'REFACTOR', 'PERFORMANCE', 'SECURITY', 'ARCHITECTURE', 'MAINTENANCE', 'TESTING', 'RELEASE'];\n\nconst CHECKS_BY_TYPE: Record<TaskType, string[]> = {\n QUESTION: [],\n DOCUMENTATION: [],\n BUG: ['unit', 'typecheck', 'lint', 'build'],\n FEATURE: ['unit', 'typecheck', 'lint', 'build', 'review'],\n REFACTOR: ['unit', 'typecheck', 'lint', 'build', 'review'],\n TESTING: ['unit', 'typecheck', 'lint'],\n PERFORMANCE: ['unit', 'build', 'review'],\n SECURITY: ['unit', 'typecheck', 'lint', 'build', 'review'],\n ARCHITECTURE: ['unit', 'typecheck', 'lint', 'build', 'review'],\n RELEASE: ['unit', 'typecheck', 'lint', 'build', 'integration', 'e2e'],\n MAINTENANCE: ['lint', 'build'],\n};\n\nexport function isCodeChanging(type: TaskType): boolean {\n return CODE_CHANGING.includes(type);\n}\n\n/** Verification a task of this type needs, given what the project actually offers. Unavailable checks are never demanded. */\nexport function requiredChecksFor(type: TaskType, contract: CommandEntry[], reviewLevel: 'off' | 'light' | 'strict' = 'light'): VerificationItem[] {\n const available = new Set(contract.filter((c) => c.available).map((c) => c.id as string));\n const items: VerificationItem[] = [];\n for (const check of CHECKS_BY_TYPE[type]) {\n if (check === 'review') {\n if (reviewLevel === 'off') continue;\n } else if (!available.has(check)) continue;\n items.push({ check, required: true, status: 'PENDING' });\n }\n return items;\n}\n\nexport function newTaskId(now: Date = new Date()): string {\n const d = now.toISOString().slice(0, 10).replace(/-/g, '');\n return `T-${d}-${randomId(2)}`;\n}\n\nexport interface NewTaskInput {\n title: string;\n type: TaskType;\n description?: string;\n verification: VerificationItem[];\n constraints?: string[];\n}\n\nexport function createTaskObject(input: NewTaskInput, now: string, id = newTaskId()): Task {\n const title = input.title.trim().slice(0, 200);\n if (!title) throw new DevWorkspaceError('USAGE', 'Task title must not be empty.');\n const task: Task = {\n schemaVersion: SCHEMA_VERSION,\n id,\n title,\n type: input.type,\n status: 'IDEA',\n createdAt: now,\n updatedAt: now,\n description: (input.description ?? '').trim().slice(0, 4000),\n acceptanceCriteria: [],\n plan: [],\n affectedAreas: [],\n verification: input.verification,\n tests: {},\n attestations: { userWorkIntact: false, knowledgeReviewed: false, finalStateInspected: false },\n decisions: [],\n blockers: [],\n constraints: input.constraints ?? [],\n history: [{ at: now, from: null, to: 'IDEA' }],\n };\n assertNoSecrets(task);\n return task;\n}\n\nexport function assertNoSecrets(task: Task): void {\n if (containsSecret(JSON.stringify(task))) {\n throw new DevWorkspaceError('SECURITY', 'Task content looks like it contains a secret; refusing to store it.', {\n hint: 'Remove credential values from titles, plans and notes.',\n });\n }\n}\n\nexport function validateTask(v: unknown, file = 'task'): Task {\n const c = new Check(file);\n const o = c.obj(v);\n c.at_('schemaVersion').int(o.schemaVersion, 1);\n c.at_('id').str(o.id, { min: 3, max: 64 });\n c.at_('title').str(o.title, { min: 1, max: 300 });\n c.at_('type').oneOf(o.type, TASK_TYPES);\n c.at_('status').oneOf(o.status, TASK_STATUSES);\n c.at_('createdAt').iso(o.createdAt);\n c.at_('updatedAt').iso(o.updatedAt);\n for (const k of ['acceptanceCriteria', 'plan', 'affectedAreas', 'decisions', 'blockers', 'constraints'] as const) c.at_(k).strArr(o[k]);\n c.at_('verification')\n .arr(o.verification, 100)\n .forEach((item, i) => {\n const ic = c.at_(`verification.${i}`);\n const io = ic.obj(item);\n ic.at_('check').str(io.check, { max: 50 });\n ic.at_('required').bool(io.required);\n ic.at_('status').oneOf(io.status, VERIFY_STATUSES);\n });\n const a = c.at_('attestations').obj(o.attestations);\n c.at_('attestations.userWorkIntact').bool(a.userWorkIntact);\n c.at_('attestations.knowledgeReviewed').bool(a.knowledgeReviewed);\n c.at_('attestations.finalStateInspected').bool(a.finalStateInspected);\n c.at_('tests').obj(o.tests);\n c.at_('history').arr(o.history, 500);\n return o as unknown as Task;\n}\n\nexport interface GateResult {\n ok: boolean;\n failures: string[];\n warnings: string[];\n}\n\n/** The completion gate. A task is only COMPLETED when every applicable requirement holds. */\nexport function evaluateGate(task: Task): GateResult {\n const failures: string[] = [];\n const warnings: string[] = [];\n for (const v of task.verification) {\n if (!v.required) continue;\n if (v.status === 'FAIL') failures.push(`${v.check}: FAIL`);\n else if (v.status === 'PENDING') failures.push(`${v.check}: not run yet`);\n else if (v.status === 'SKIPPED') {\n if (v.reason === 'user-override') warnings.push(`${v.check}: skipped at the user's request; completion is less certain`);\n else failures.push(`${v.check}: skipped without a user override`);\n }\n }\n const { baseline, after } = task.tests;\n if (baseline && !after) failures.push('baseline test results recorded, but no results after the change');\n if (baseline && after) {\n if (after.failed > baseline.failed) failures.push(`regression: failures rose from ${baseline.failed} to ${after.failed}`);\n if (after.passed < baseline.passed) failures.push(`regression: passing tests fell from ${baseline.passed} to ${after.passed}`);\n }\n if (isCodeChanging(task.type)) {\n if (!task.attestations.userWorkIntact) failures.push('not confirmed: no user work was overwritten');\n if (!task.attestations.knowledgeReviewed) failures.push('not confirmed: project knowledge was reviewed for updates');\n if (!task.attestations.finalStateInspected) failures.push('not confirmed: final repository state was inspected');\n }\n if ((isCodeChanging(task.type) || task.type === 'DOCUMENTATION') && !(task.completionSummary ?? '').trim()) {\n failures.push('completion summary is missing');\n }\n return { ok: failures.length === 0, failures, warnings };\n}\n\nexport function transitionTask(task: Task, to: TaskStatus, now: string, note?: string): GateResult | null {\n if (task.status === to) return null;\n if (!TRANSITIONS[task.status].includes(to)) {\n throw new DevWorkspaceError('GATE', `Cannot move a task from ${task.status} to ${to}.`, {\n hint: `Allowed from ${task.status}: ${TRANSITIONS[task.status].join(', ')}.`,\n });\n }\n let gate: GateResult | null = null;\n if (to === 'COMPLETED') {\n gate = evaluateGate(task);\n if (!gate.ok) {\n throw new DevWorkspaceError('GATE', `Task ${task.id} cannot be completed:\\n - ${gate.failures.join('\\n - ')}`, {\n hint: 'Resolve the items above (or mark the task BLOCKED/FAILED). A task is never complete while verification is failing.',\n });\n }\n }\n task.history.push({ at: now, from: task.status, to, ...(note ? { note: note.slice(0, 300) } : {}) });\n task.status = to;\n task.updatedAt = now;\n return gate;\n}\n\nexport function nextRequiredStep(task: Task): string {\n switch (task.status) {\n case 'COMPLETED':\n return 'None. The task is complete.';\n case 'FAILED':\n return 'Review the failure, then move the task back to IN_PROGRESS or close the idea.';\n case 'BLOCKED':\n return task.blockers.length > 0 ? `Resolve blocker: ${task.blockers[task.blockers.length - 1]}` : 'Identify and record what blocks this task.';\n case 'IDEA':\n return 'Clarify acceptance criteria, then plan.';\n case 'SPECIFIED':\n return 'Write a plan.';\n case 'PLANNED':\n return 'Start implementation (baseline verification first for meaningful changes).';\n default: {\n const pending = task.verification.filter((v) => v.required && (v.status === 'PENDING' || v.status === 'FAIL'));\n if (pending.length > 0) return `Run/fix verification: ${pending.map((p) => `${p.check} (${p.status.toLowerCase()})`).join(', ')}.`;\n const g = evaluateGate(task);\n return g.ok ? 'All gates pass. Mark the task COMPLETED.' : `Satisfy the completion gate: ${g.failures[0]}`;\n }\n }\n}\n\n// ---------- storage ----------\n\nfunction taskFile(ws: Ws, task: Pick<Task, 'id' | 'status'>): string {\n return path.join(task.status === 'COMPLETED' ? ws.tasksCompleted : ws.tasksActive, `${task.id}.json`);\n}\n\nexport async function saveTask(ws: Ws, task: Task): Promise<void> {\n validateTask(task);\n assertNoSecrets(task);\n const dest = taskFile(ws, task);\n await backupIfVersionChanges(ws, dest, task.schemaVersion);\n await ensureDir(path.dirname(dest));\n await atomicWriteJson(dest, task);\n const other = path.join(task.status === 'COMPLETED' ? ws.tasksActive : ws.tasksCompleted, `${task.id}.json`);\n await fs.rm(other, { force: true });\n}\n\nexport async function loadTask(ws: Ws, idOrPrefix: string): Promise<Task> {\n for (const dir of [ws.tasksActive, ws.tasksCompleted]) {\n if (!(await pathExists(dir))) continue;\n const names = (await fs.readdir(dir)).filter((n) => n.endsWith('.json'));\n const hits = names.filter((n) => n === `${idOrPrefix}.json` || n.startsWith(idOrPrefix));\n if (hits.length > 1) {\n const exact = hits.find((n) => n === `${idOrPrefix}.json`);\n if (!exact) throw new DevWorkspaceError('USAGE', `Task id \"${idOrPrefix}\" is ambiguous.`, { hint: 'Use more characters of the id.' });\n hits.splice(0, hits.length, exact);\n }\n if (hits.length === 1) {\n const file = path.join(dir, hits[0]!);\n return (await readVersioned(file, 'task', SCHEMA_VERSION, validateTask)).data;\n }\n }\n throw new DevWorkspaceError('NOT_FOUND', `No task matching \"${idOrPrefix}\".`, { hint: 'List tasks with \"devworkspace task list --all\".' });\n}\n\nexport interface TaskListing {\n tasks: Task[];\n corrupt: { file: string; error: string }[];\n}\n\nexport async function listTasks(ws: Ws, opts: { includeCompleted?: boolean } = {}): Promise<TaskListing> {\n const tasks: Task[] = [];\n const corrupt: { file: string; error: string }[] = [];\n const dirs = opts.includeCompleted ? [ws.tasksActive, ws.tasksCompleted] : [ws.tasksActive];\n for (const dir of dirs) {\n if (!(await pathExists(dir))) continue;\n for (const name of (await fs.readdir(dir)).filter((n) => n.endsWith('.json')).sort()) {\n try {\n tasks.push((await readVersioned(path.join(dir, name), 'task', SCHEMA_VERSION, validateTask)).data);\n } catch (e) {\n corrupt.push({ file: `${path.basename(dir)}/${name}`, error: (e as Error).message });\n }\n }\n }\n tasks.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));\n return { tasks, corrupt };\n}\n"]}
1
+ {"version":3,"file":"task.js","sourceRoot":"","sources":["../../src/tasks/task.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport { Check } from '../config/validate.js';\nimport { SCHEMA_VERSION, type CommandEntry } from '../config/schemas.js';\nimport { atomicWriteJson, ensureDir, pathExists, randomId } from '../utils/fs.js';\nimport { readVersioned } from '../core/versioned.js';\nimport { backupIfVersionChanges, type Ws } from '../core/workspace.js';\nimport { containsSecret } from '../security/redact.js';\n\nexport const TASK_TYPES = [\n 'QUESTION',\n 'BUG',\n 'FEATURE',\n 'REFACTOR',\n 'DOCUMENTATION',\n 'TESTING',\n 'PERFORMANCE',\n 'SECURITY',\n 'ARCHITECTURE',\n 'RELEASE',\n 'MAINTENANCE',\n] as const;\nexport type TaskType = (typeof TASK_TYPES)[number];\n\nexport const TASK_STATUSES = ['IDEA', 'SPECIFIED', 'PLANNED', 'IN_PROGRESS', 'TESTING', 'REVIEW', 'BLOCKED', 'FAILED', 'COMPLETED'] as const;\nexport type TaskStatus = (typeof TASK_STATUSES)[number];\n\nexport const VERIFY_STATUSES = ['PENDING', 'PASS', 'FAIL', 'SKIPPED', 'COULD_NOT_RUN'] as const;\nexport type VerifyStatus = (typeof VERIFY_STATUSES)[number];\n\n/**\n * One recorded result for one package. `executed` means DevWorkspace ran the contract command itself and saw the exit\n * code; `claimed` is somebody's report (agent or person) that DevWorkspace could not observe.\n */\nexport interface Evidence {\n kind: 'executed' | 'claimed';\n at: string;\n /** Package directory relative to the project root; \".\" for the root. */\n package: string;\n command?: string;\n exitCode?: number | null;\n durationMs?: number;\n /** Fingerprint of the task's changes inside this package when the result was recorded. */\n scope?: string;\n /** A claim the user explicitly vouched for (--reason user-override). */\n userOverride?: boolean;\n /** One-line, scrubbed explanation (failure tail or refusal reason). Never a connection string or credential. */\n note?: string;\n}\n\nexport interface VerificationItem {\n check: string;\n required: boolean;\n status: VerifyStatus;\n reason?: string;\n detail?: string;\n updatedAt?: string;\n /** Latest result per package. Absent on tasks recorded by older versions. */\n evidence?: Evidence[];\n}\n\n/** Files that were already modified when the task was created, so they are not blamed on the task. */\nexport interface TaskScope {\n capturedAt: string;\n /** Git HEAD when the task was created; null outside a repository. */\n head: string | null;\n /** Project-relative path -> content hash of files already modified at creation (\"deleted\" for removed files). */\n baseline: Record<string, string>;\n}\n\nexport interface TestCounts {\n passed: number;\n failed: number;\n}\n\nexport interface Attestations {\n userWorkIntact: boolean;\n knowledgeReviewed: boolean;\n finalStateInspected: boolean;\n}\n\nexport interface Task {\n schemaVersion: number;\n id: string;\n title: string;\n type: TaskType;\n status: TaskStatus;\n createdAt: string;\n updatedAt: string;\n description: string;\n acceptanceCriteria: string[];\n plan: string[];\n affectedAreas: string[];\n verification: VerificationItem[];\n tests: { baseline?: TestCounts; after?: TestCounts };\n attestations: Attestations;\n /** Present on tasks created by 0.2.2+: enables package-aware, evidence-based completion. */\n scope?: TaskScope;\n decisions: string[];\n blockers: string[];\n constraints: string[];\n completionSummary?: string;\n history: { at: string; from: TaskStatus | null; to: TaskStatus; note?: string }[];\n}\n\nconst TRANSITIONS: Record<TaskStatus, TaskStatus[]> = {\n IDEA: ['SPECIFIED', 'PLANNED', 'IN_PROGRESS', 'BLOCKED', 'FAILED'],\n SPECIFIED: ['PLANNED', 'IN_PROGRESS', 'BLOCKED', 'IDEA'],\n PLANNED: ['IN_PROGRESS', 'SPECIFIED', 'BLOCKED', 'IDEA'],\n IN_PROGRESS: ['TESTING', 'REVIEW', 'COMPLETED', 'BLOCKED', 'FAILED', 'PLANNED'],\n TESTING: ['IN_PROGRESS', 'REVIEW', 'COMPLETED', 'BLOCKED', 'FAILED'],\n REVIEW: ['IN_PROGRESS', 'TESTING', 'COMPLETED', 'BLOCKED', 'FAILED'],\n BLOCKED: ['IN_PROGRESS', 'PLANNED', 'SPECIFIED', 'IDEA', 'FAILED'],\n FAILED: ['IN_PROGRESS', 'IDEA'],\n COMPLETED: ['IN_PROGRESS'],\n};\n\nexport function allowedTransitions(from: TaskStatus): TaskStatus[] {\n return TRANSITIONS[from];\n}\n\nconst CODE_CHANGING: TaskType[] = ['BUG', 'FEATURE', 'REFACTOR', 'PERFORMANCE', 'SECURITY', 'ARCHITECTURE', 'MAINTENANCE', 'TESTING', 'RELEASE'];\n\nconst CHECKS_BY_TYPE: Record<TaskType, string[]> = {\n QUESTION: [],\n DOCUMENTATION: [],\n BUG: ['unit', 'typecheck', 'lint', 'build'],\n FEATURE: ['unit', 'typecheck', 'lint', 'build', 'review'],\n REFACTOR: ['unit', 'typecheck', 'lint', 'build', 'review'],\n TESTING: ['unit', 'typecheck', 'lint'],\n PERFORMANCE: ['unit', 'build', 'review'],\n SECURITY: ['unit', 'typecheck', 'lint', 'build', 'review'],\n ARCHITECTURE: ['unit', 'typecheck', 'lint', 'build', 'review'],\n RELEASE: ['unit', 'typecheck', 'lint', 'build', 'integration', 'e2e'],\n MAINTENANCE: ['lint', 'build'],\n};\n\nexport function isCodeChanging(type: TaskType): boolean {\n return CODE_CHANGING.includes(type);\n}\n\n/** Verification a task of this type needs, given what the project actually offers. Unavailable checks are never demanded. */\nexport function requiredChecksFor(type: TaskType, contract: CommandEntry[], reviewLevel: 'off' | 'light' | 'strict' = 'light'): VerificationItem[] {\n const available = new Set(contract.filter((c) => c.available).map((c) => c.id as string));\n const items: VerificationItem[] = [];\n for (const check of CHECKS_BY_TYPE[type]) {\n if (check === 'review') {\n if (reviewLevel === 'off') continue;\n } else if (!available.has(check)) continue;\n items.push({ check, required: true, status: 'PENDING' });\n }\n return items;\n}\n\nexport function newTaskId(now: Date = new Date()): string {\n const d = now.toISOString().slice(0, 10).replace(/-/g, '');\n return `T-${d}-${randomId(2)}`;\n}\n\nexport interface NewTaskInput {\n title: string;\n type: TaskType;\n description?: string;\n verification: VerificationItem[];\n constraints?: string[];\n}\n\nexport function createTaskObject(input: NewTaskInput, now: string, id = newTaskId()): Task {\n const title = input.title.trim().slice(0, 200);\n if (!title) throw new DevWorkspaceError('USAGE', 'Task title must not be empty.');\n const task: Task = {\n schemaVersion: SCHEMA_VERSION,\n id,\n title,\n type: input.type,\n status: 'IDEA',\n createdAt: now,\n updatedAt: now,\n description: (input.description ?? '').trim().slice(0, 4000),\n acceptanceCriteria: [],\n plan: [],\n affectedAreas: [],\n verification: input.verification,\n tests: {},\n attestations: { userWorkIntact: false, knowledgeReviewed: false, finalStateInspected: false },\n decisions: [],\n blockers: [],\n constraints: input.constraints ?? [],\n history: [{ at: now, from: null, to: 'IDEA' }],\n };\n assertNoSecrets(task);\n return task;\n}\n\nexport function assertNoSecrets(task: Task): void {\n if (containsSecret(JSON.stringify(task))) {\n throw new DevWorkspaceError('SECURITY', 'Task content looks like it contains a secret; refusing to store it.', {\n hint: 'Remove credential values from titles, plans and notes.',\n });\n }\n}\n\nexport function validateTask(v: unknown, file = 'task'): Task {\n const c = new Check(file);\n const o = c.obj(v);\n c.at_('schemaVersion').int(o.schemaVersion, 1);\n c.at_('id').str(o.id, { min: 3, max: 64 });\n c.at_('title').str(o.title, { min: 1, max: 300 });\n c.at_('type').oneOf(o.type, TASK_TYPES);\n c.at_('status').oneOf(o.status, TASK_STATUSES);\n c.at_('createdAt').iso(o.createdAt);\n c.at_('updatedAt').iso(o.updatedAt);\n for (const k of ['acceptanceCriteria', 'plan', 'affectedAreas', 'decisions', 'blockers', 'constraints'] as const) c.at_(k).strArr(o[k]);\n c.at_('verification')\n .arr(o.verification, 100)\n .forEach((item, i) => {\n const ic = c.at_(`verification.${i}`);\n const io = ic.obj(item);\n ic.at_('check').str(io.check, { max: 50 });\n ic.at_('required').bool(io.required);\n ic.at_('status').oneOf(io.status, VERIFY_STATUSES);\n if (io.evidence !== undefined) {\n ic.at_('evidence')\n .arr(io.evidence, 100)\n .forEach((e, j) => {\n const ec = c.at_(`verification.${i}.evidence.${j}`);\n const eo = ec.obj(e);\n ec.at_('kind').oneOf(eo.kind, ['executed', 'claimed'] as const);\n ec.at_('package').str(eo.package, { max: 240 });\n });\n }\n });\n if (o.scope !== undefined) {\n const sc = c.at_('scope').obj(o.scope);\n c.at_('scope.capturedAt').iso(sc.capturedAt);\n c.at_('scope.baseline').obj(sc.baseline);\n }\n const a = c.at_('attestations').obj(o.attestations);\n c.at_('attestations.userWorkIntact').bool(a.userWorkIntact);\n c.at_('attestations.knowledgeReviewed').bool(a.knowledgeReviewed);\n c.at_('attestations.finalStateInspected').bool(a.finalStateInspected);\n c.at_('tests').obj(o.tests);\n c.at_('history').arr(o.history, 500);\n return o as unknown as Task;\n}\n\nexport interface GateResult {\n ok: boolean;\n failures: string[];\n warnings: string[];\n}\n\n/** What the gate needs to know beyond the task itself: the contract and what the task changed, per package. */\nexport interface GateContext {\n contract: CommandEntry[];\n /**\n * Fingerprint of the task's changes inside each package (\".\" = everything). `null` means the changes could not be\n * determined (not a Git repository, or nothing changed since the task started), so every package counts as affected.\n */\n scope: Record<string, string> | null;\n}\n\nconst UNKNOWN_SCOPE = 'unknown';\n\n/** Per-package requirements of one check. Returns false when the contract has no applicable package entry. */\nfunction packageGate(v: VerificationItem, ctx: GateContext, failures: string[], warnings: string[]): boolean {\n const entries = ctx.contract.filter((c) => c.id === v.check && c.available && !c.mutates);\n if (entries.length === 0) return false;\n const known = ctx.scope !== null;\n const unknown = !known;\n let applicable = 0;\n for (const entry of entries) {\n const pkg = entry.cwd ?? '.';\n const fp = known ? ctx.scope![pkg] : UNKNOWN_SCOPE;\n if (fp === undefined) continue; // this package was not changed, so this check does not apply to it\n applicable++;\n const label = `${v.check} [${pkg}]`;\n const ev = v.evidence?.find((e) => e.package === pkg);\n if (!ev) failures.push(`${label}: not run for the current changes`);\n else if (ev.kind === 'claimed' && !ev.userOverride) failures.push(`${label}: only claimed, not executed (run: devworkspace task run <id> ${v.check} --package ${pkg})`);\n else if (ev.kind === 'claimed') warnings.push(`${label}: accepted on the user's word; DevWorkspace did not observe it run`);\n else if (ev.exitCode !== 0) failures.push(`${label}: FAIL (exit ${ev.exitCode ?? 'none'})`);\n else if (!unknown && ev.scope !== fp) failures.push(`${label}: passed, but files changed afterwards; run it again`);\n }\n if (known && applicable === 0) warnings.push(`${v.check}: no package with this check was changed`);\n return true;\n}\n\n/**\n * The completion gate. A task is only COMPLETED when every applicable requirement holds.\n * Tasks that carry a `scope` (created by 0.2.2+) are checked per changed package against executed evidence; older\n * tasks keep the original rules.\n */\nexport function evaluateGate(task: Task, ctx?: GateContext): GateResult {\n const failures: string[] = [];\n const warnings: string[] = [];\n const scoped = !!ctx && !!task.scope;\n for (const v of task.verification) {\n if (!v.required) continue;\n if (v.status === 'FAIL') failures.push(`${v.check}: FAIL`);\n else if (v.status === 'COULD_NOT_RUN') failures.push(`${v.check}: could not run${v.detail ? ` (${v.detail})` : ''}`);\n else if (v.status === 'SKIPPED') {\n if (v.reason === 'user-override') warnings.push(`${v.check}: skipped at the user's request; completion is less certain`);\n else failures.push(`${v.check}: skipped without a user override`);\n } else if (scoped && packageGate(v, ctx!, failures, warnings)) {\n // handled per package\n } else if (v.status === 'PENDING') failures.push(`${v.check}: not run yet`);\n }\n const { baseline, after } = task.tests;\n if (baseline && !after) failures.push('baseline test results recorded, but no results after the change');\n if (baseline && after) {\n if (after.failed > baseline.failed) failures.push(`regression: failures rose from ${baseline.failed} to ${after.failed}`);\n if (after.passed < baseline.passed) failures.push(`regression: passing tests fell from ${baseline.passed} to ${after.passed}`);\n }\n if (isCodeChanging(task.type)) {\n if (!task.attestations.userWorkIntact) failures.push('not confirmed: no user work was overwritten');\n if (!task.attestations.knowledgeReviewed) failures.push('not confirmed: project knowledge was reviewed for updates');\n if (!task.attestations.finalStateInspected) failures.push('not confirmed: final repository state was inspected');\n }\n if ((isCodeChanging(task.type) || task.type === 'DOCUMENTATION') && !(task.completionSummary ?? '').trim()) {\n failures.push('completion summary is missing');\n }\n return { ok: failures.length === 0, failures, warnings };\n}\n\nexport function transitionTask(task: Task, to: TaskStatus, now: string, note?: string, ctx?: GateContext): GateResult | null {\n if (task.status === to) return null;\n if (!TRANSITIONS[task.status].includes(to)) {\n throw new DevWorkspaceError('GATE', `Cannot move a task from ${task.status} to ${to}.`, {\n hint: `Allowed from ${task.status}: ${TRANSITIONS[task.status].join(', ')}.`,\n });\n }\n let gate: GateResult | null = null;\n if (to === 'COMPLETED') {\n gate = evaluateGate(task, ctx);\n if (!gate.ok) {\n throw new DevWorkspaceError('GATE', `Task ${task.id} cannot be completed:\\n - ${gate.failures.join('\\n - ')}`, {\n hint: 'Resolve the items above (or mark the task BLOCKED/FAILED). A task is never complete while verification is failing.',\n });\n }\n }\n task.history.push({ at: now, from: task.status, to, ...(note ? { note: note.slice(0, 300) } : {}) });\n task.status = to;\n task.updatedAt = now;\n return gate;\n}\n\nexport function nextRequiredStep(task: Task, ctx?: GateContext): string {\n switch (task.status) {\n case 'COMPLETED':\n return 'None. The task is complete.';\n case 'FAILED':\n return 'Review the failure, then move the task back to IN_PROGRESS or close the idea.';\n case 'BLOCKED':\n return task.blockers.length > 0 ? `Resolve blocker: ${task.blockers[task.blockers.length - 1]}` : 'Identify and record what blocks this task.';\n case 'IDEA':\n return 'Clarify acceptance criteria, then plan.';\n case 'SPECIFIED':\n return 'Write a plan.';\n case 'PLANNED':\n return 'Start implementation (baseline verification first for meaningful changes).';\n default: {\n const pending = task.verification.filter((v) => v.required && (v.status === 'PENDING' || v.status === 'FAIL' || v.status === 'COULD_NOT_RUN'));\n if (pending.length > 0) return `Run/fix verification: ${pending.map((p) => `${p.check} (${p.status.toLowerCase().replace(/_/g, ' ')})`).join(', ')}.`;\n const g = evaluateGate(task, ctx);\n return g.ok ? 'All gates pass. Mark the task COMPLETED.' : `Satisfy the completion gate: ${g.failures[0]}`;\n }\n }\n}\n\n// ---------- storage ----------\n\nfunction taskFile(ws: Ws, task: Pick<Task, 'id' | 'status'>): string {\n return path.join(task.status === 'COMPLETED' ? ws.tasksCompleted : ws.tasksActive, `${task.id}.json`);\n}\n\nexport async function saveTask(ws: Ws, task: Task): Promise<void> {\n validateTask(task);\n assertNoSecrets(task);\n const dest = taskFile(ws, task);\n await backupIfVersionChanges(ws, dest, task.schemaVersion);\n await ensureDir(path.dirname(dest));\n await atomicWriteJson(dest, task);\n const other = path.join(task.status === 'COMPLETED' ? ws.tasksActive : ws.tasksCompleted, `${task.id}.json`);\n await fs.rm(other, { force: true });\n}\n\nexport async function loadTask(ws: Ws, idOrPrefix: string): Promise<Task> {\n for (const dir of [ws.tasksActive, ws.tasksCompleted]) {\n if (!(await pathExists(dir))) continue;\n const names = (await fs.readdir(dir)).filter((n) => n.endsWith('.json'));\n const hits = names.filter((n) => n === `${idOrPrefix}.json` || n.startsWith(idOrPrefix));\n if (hits.length > 1) {\n const exact = hits.find((n) => n === `${idOrPrefix}.json`);\n if (!exact) throw new DevWorkspaceError('USAGE', `Task id \"${idOrPrefix}\" is ambiguous.`, { hint: 'Use more characters of the id.' });\n hits.splice(0, hits.length, exact);\n }\n if (hits.length === 1) {\n const file = path.join(dir, hits[0]!);\n return (await readVersioned(file, 'task', SCHEMA_VERSION, validateTask)).data;\n }\n }\n throw new DevWorkspaceError('NOT_FOUND', `No task matching \"${idOrPrefix}\".`, { hint: 'List tasks with \"devworkspace task list --all\".' });\n}\n\nexport interface TaskListing {\n tasks: Task[];\n corrupt: { file: string; error: string }[];\n}\n\nexport async function listTasks(ws: Ws, opts: { includeCompleted?: boolean } = {}): Promise<TaskListing> {\n const tasks: Task[] = [];\n const corrupt: { file: string; error: string }[] = [];\n const dirs = opts.includeCompleted ? [ws.tasksActive, ws.tasksCompleted] : [ws.tasksActive];\n for (const dir of dirs) {\n if (!(await pathExists(dir))) continue;\n for (const name of (await fs.readdir(dir)).filter((n) => n.endsWith('.json')).sort()) {\n try {\n tasks.push((await readVersioned(path.join(dir, name), 'task', SCHEMA_VERSION, validateTask)).data);\n } catch (e) {\n corrupt.push({ file: `${path.basename(dir)}/${name}`, error: (e as Error).message });\n }\n }\n }\n tasks.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));\n return { tasks, corrupt };\n}\n"]}
@@ -0,0 +1,38 @@
1
+ import type { CommandEntry } from '../config/schemas.js';
2
+ import type { Ws } from '../core/workspace.js';
3
+ export type Safety = 'safe' | 'needs-test-environment' | 'blocked';
4
+ export interface CommandSafety {
5
+ safety: Safety;
6
+ reasons: string[];
7
+ /** Services the package is known to use (from its manifest), for the explanation only. */
8
+ services: string[];
9
+ }
10
+ export interface ManifestEvidence {
11
+ scriptBody: string | null;
12
+ services: string[];
13
+ hasCredentialFile: boolean;
14
+ }
15
+ /** Read the package manifest next to a command: the script it runs and the services the package depends on. Never reads .env files. */
16
+ export declare function readManifestEvidence(root: string, entry: CommandEntry): Promise<ManifestEvidence>;
17
+ /**
18
+ * Decide whether DevWorkspace may run a contract command by itself. Pure; no file is read here.
19
+ * - blocked: modifies project files, changes a schema/data set, or is destructive. Never run automatically.
20
+ * - needs-test-environment: exercises real services or may (a catch-all `test` in a package that uses a database).
21
+ * Runs only after the user declared a test environment for that package and check.
22
+ * - safe: typecheck, lint, build, unit tests and read-only format checks.
23
+ * A database is never judged safe from its name; DevWorkspace does not read connection settings at all.
24
+ */
25
+ export declare function classifyCommand(entry: CommandEntry, evidence: ManifestEvidence): CommandSafety;
26
+ export interface TestEnvDeclaration {
27
+ /** Package directory relative to the project root; "." for the root. */
28
+ package: string;
29
+ checks: string[];
30
+ /** Human description of the target, e.g. "docker compose test stack on this laptop". Never a connection string. */
31
+ target: string;
32
+ declaredAt: string;
33
+ }
34
+ export declare function loadTestEnv(ws: Ws): Promise<TestEnvDeclaration[]>;
35
+ export declare function assertSafeTarget(target: string): void;
36
+ export declare function saveDeclaration(ws: Ws, d: TestEnvDeclaration): Promise<void>;
37
+ export declare function removeDeclaration(ws: Ws, pkg: string): Promise<boolean>;
38
+ export declare function isDeclared(decls: TestEnvDeclaration[], pkg: string, check: string): TestEnvDeclaration | undefined;
@@ -0,0 +1,120 @@
1
+ import path from 'node:path';
2
+ import { DevWorkspaceError } from '../errors/index.js';
3
+ import { containsSecret } from '../security/redact.js';
4
+ import { safeReadText } from '../security/files.js';
5
+ import { atomicWriteJson, pathExists, readJsonFile } from '../utils/fs.js';
6
+ /** Dependencies that mean a package talks to a database or queue. */
7
+ const SERVICE_DEPS = {
8
+ pg: 'PostgreSQL',
9
+ postgres: 'PostgreSQL',
10
+ 'drizzle-orm': 'a SQL database',
11
+ prisma: 'a SQL database',
12
+ '@prisma/client': 'a SQL database',
13
+ typeorm: 'a SQL database',
14
+ sequelize: 'a SQL database',
15
+ knex: 'a SQL database',
16
+ mongoose: 'MongoDB',
17
+ mongodb: 'MongoDB',
18
+ redis: 'Redis',
19
+ ioredis: 'Redis',
20
+ bullmq: 'Redis',
21
+ bull: 'Redis',
22
+ };
23
+ /** Words that make a command destructive or schema-changing no matter which check it is filed under. */
24
+ const DESTRUCTIVE = /(?:^|[\s:/_-])(?:drop|truncate|flushall|flushdb|reset|seed|migrate)(?:$|[\s:/_.-])|db:push|--force|rm\s+-rf/i;
25
+ /** Checks whose job is to exercise real services. They need a declared test environment. */
26
+ const SERVICE_CHECKS = new Set(['integration', 'security', 'e2e']);
27
+ /** Read the package manifest next to a command: the script it runs and the services the package depends on. Never reads .env files. */
28
+ export async function readManifestEvidence(root, entry) {
29
+ const dir = entry.cwd && entry.cwd !== '.' ? entry.cwd : '';
30
+ const manifest = dir ? `${dir}/package.json` : 'package.json';
31
+ let scriptBody = null;
32
+ const services = new Set();
33
+ try {
34
+ const text = await safeReadText(root, manifest, 1024 * 1024);
35
+ const json = text ? JSON.parse(text) : null;
36
+ const script = /#scripts\.(.+)$/.exec(entry.source ?? '')?.[1];
37
+ if (json && script && typeof json.scripts?.[script] === 'string')
38
+ scriptBody = json.scripts[script];
39
+ for (const dep of Object.keys({ ...json?.dependencies, ...json?.devDependencies }))
40
+ if (SERVICE_DEPS[dep])
41
+ services.add(SERVICE_DEPS[dep]);
42
+ }
43
+ catch {
44
+ /* unreadable manifest: no evidence */
45
+ }
46
+ let hasCredentialFile = false;
47
+ for (const name of ['.env', '.env.local', '.env.test', '.env.development']) {
48
+ if (await pathExists(path.join(root, ...(dir ? [dir] : []), name)))
49
+ hasCredentialFile = true;
50
+ }
51
+ // "a SQL database" is only the fallback label for an ORM; drop it once a specific database is known.
52
+ if (services.size > 1)
53
+ services.delete('a SQL database');
54
+ return { scriptBody, services: [...services].sort(), hasCredentialFile };
55
+ }
56
+ /**
57
+ * Decide whether DevWorkspace may run a contract command by itself. Pure; no file is read here.
58
+ * - blocked: modifies project files, changes a schema/data set, or is destructive. Never run automatically.
59
+ * - needs-test-environment: exercises real services or may (a catch-all `test` in a package that uses a database).
60
+ * Runs only after the user declared a test environment for that package and check.
61
+ * - safe: typecheck, lint, build, unit tests and read-only format checks.
62
+ * A database is never judged safe from its name; DevWorkspace does not read connection settings at all.
63
+ */
64
+ export function classifyCommand(entry, evidence) {
65
+ const reasons = [];
66
+ const services = evidence.services;
67
+ const text = `${entry.command ?? ''} ${evidence.scriptBody ?? ''}`;
68
+ if (entry.mutates)
69
+ return { safety: 'blocked', reasons: ['it modifies project files; DevWorkspace never runs formatters or fixers'], services };
70
+ if (entry.id === 'migrations')
71
+ return { safety: 'blocked', reasons: ['it changes a database schema; DevWorkspace never runs migrations'], services };
72
+ if (DESTRUCTIVE.test(text))
73
+ return { safety: 'blocked', reasons: ['its definition mentions a destructive or schema-changing operation (drop/reset/seed/migrate/flush)'], services };
74
+ if (SERVICE_CHECKS.has(entry.id)) {
75
+ reasons.push(`${entry.id} tests exercise real services${services.length ? ` (the package uses ${services.join(', ')})` : ''}`);
76
+ }
77
+ else if (entry.id === 'unit' && services.length > 0 && /(?:^|\s)(?:run\s+)?test(?:\s|$)/.test(entry.command ?? '') && !/unit/.test(entry.command ?? '')) {
78
+ reasons.push(`the catch-all test script of a package that uses ${services.join(', ')} may include integration tests`);
79
+ }
80
+ if (reasons.length === 0)
81
+ return { safety: 'safe', reasons: [], services };
82
+ if (evidence.hasCredentialFile)
83
+ reasons.push('a credential file exists in the package (never read), so the database target cannot be verified');
84
+ else
85
+ reasons.push('the database target cannot be verified');
86
+ return { safety: 'needs-test-environment', reasons, services };
87
+ }
88
+ const file = (ws) => path.join(ws.localDir, 'test-environment.json');
89
+ export async function loadTestEnv(ws) {
90
+ if (!(await pathExists(file(ws))))
91
+ return [];
92
+ const raw = (await readJsonFile(file(ws)));
93
+ return Array.isArray(raw.declarations) ? raw.declarations : [];
94
+ }
95
+ export function assertSafeTarget(target) {
96
+ const t = target.trim();
97
+ if (t.length < 5 || t.length > 200)
98
+ throw new DevWorkspaceError('USAGE', '--target must be a short description (5-200 characters) of the test environment.');
99
+ if (containsSecret(t) || /[a-z][a-z0-9+.-]*:\/\/\S+/i.test(t) || /(?:password|passwd|pwd|token|secret)\s*[=:]/i.test(t)) {
100
+ throw new DevWorkspaceError('SECURITY', 'The target description looks like a connection string or credential; refusing to store it.', {
101
+ hint: 'Describe the environment in words (for example "disposable docker compose stack"), not how to connect to it.',
102
+ });
103
+ }
104
+ }
105
+ export async function saveDeclaration(ws, d) {
106
+ const all = (await loadTestEnv(ws)).filter((x) => x.package !== d.package);
107
+ await atomicWriteJson(file(ws), { schemaVersion: 1, declarations: [...all, d] });
108
+ }
109
+ export async function removeDeclaration(ws, pkg) {
110
+ const all = await loadTestEnv(ws);
111
+ const rest = all.filter((x) => x.package !== pkg);
112
+ if (rest.length === all.length)
113
+ return false;
114
+ await atomicWriteJson(file(ws), { schemaVersion: 1, declarations: rest });
115
+ return true;
116
+ }
117
+ export function isDeclared(decls, pkg, check) {
118
+ return decls.find((d) => d.package === pkg && d.checks.includes(check));
119
+ }
120
+ //# sourceMappingURL=safety.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safety.js","sourceRoot":"","sources":["../../src/verification/safety.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path';\nimport type { CommandEntry } from '../config/schemas.js';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport { containsSecret } from '../security/redact.js';\nimport { safeReadText } from '../security/files.js';\nimport { atomicWriteJson, pathExists, readJsonFile } from '../utils/fs.js';\nimport type { Ws } from '../core/workspace.js';\n\n/** Dependencies that mean a package talks to a database or queue. */\nconst SERVICE_DEPS: Record<string, string> = {\n pg: 'PostgreSQL',\n postgres: 'PostgreSQL',\n 'drizzle-orm': 'a SQL database',\n prisma: 'a SQL database',\n '@prisma/client': 'a SQL database',\n typeorm: 'a SQL database',\n sequelize: 'a SQL database',\n knex: 'a SQL database',\n mongoose: 'MongoDB',\n mongodb: 'MongoDB',\n redis: 'Redis',\n ioredis: 'Redis',\n bullmq: 'Redis',\n bull: 'Redis',\n};\n\n/** Words that make a command destructive or schema-changing no matter which check it is filed under. */\nconst DESTRUCTIVE = /(?:^|[\\s:/_-])(?:drop|truncate|flushall|flushdb|reset|seed|migrate)(?:$|[\\s:/_.-])|db:push|--force|rm\\s+-rf/i;\n/** Checks whose job is to exercise real services. They need a declared test environment. */\nconst SERVICE_CHECKS = new Set(['integration', 'security', 'e2e']);\n\nexport type Safety = 'safe' | 'needs-test-environment' | 'blocked';\n\nexport interface CommandSafety {\n safety: Safety;\n reasons: string[];\n /** Services the package is known to use (from its manifest), for the explanation only. */\n services: string[];\n}\n\nexport interface ManifestEvidence {\n scriptBody: string | null;\n services: string[];\n hasCredentialFile: boolean;\n}\n\n/** Read the package manifest next to a command: the script it runs and the services the package depends on. Never reads .env files. */\nexport async function readManifestEvidence(root: string, entry: CommandEntry): Promise<ManifestEvidence> {\n const dir = entry.cwd && entry.cwd !== '.' ? entry.cwd : '';\n const manifest = dir ? `${dir}/package.json` : 'package.json';\n let scriptBody: string | null = null;\n const services = new Set<string>();\n try {\n const text = await safeReadText(root, manifest, 1024 * 1024);\n const json = text ? (JSON.parse(text) as { scripts?: Record<string, string>; dependencies?: Record<string, string>; devDependencies?: Record<string, string> }) : null;\n const script = /#scripts\\.(.+)$/.exec(entry.source ?? '')?.[1];\n if (json && script && typeof json.scripts?.[script] === 'string') scriptBody = json.scripts[script]!;\n for (const dep of Object.keys({ ...json?.dependencies, ...json?.devDependencies })) if (SERVICE_DEPS[dep]) services.add(SERVICE_DEPS[dep]!);\n } catch {\n /* unreadable manifest: no evidence */\n }\n let hasCredentialFile = false;\n for (const name of ['.env', '.env.local', '.env.test', '.env.development']) {\n if (await pathExists(path.join(root, ...(dir ? [dir] : []), name))) hasCredentialFile = true;\n }\n // \"a SQL database\" is only the fallback label for an ORM; drop it once a specific database is known.\n if (services.size > 1) services.delete('a SQL database');\n return { scriptBody, services: [...services].sort(), hasCredentialFile };\n}\n\n/**\n * Decide whether DevWorkspace may run a contract command by itself. Pure; no file is read here.\n * - blocked: modifies project files, changes a schema/data set, or is destructive. Never run automatically.\n * - needs-test-environment: exercises real services or may (a catch-all `test` in a package that uses a database).\n * Runs only after the user declared a test environment for that package and check.\n * - safe: typecheck, lint, build, unit tests and read-only format checks.\n * A database is never judged safe from its name; DevWorkspace does not read connection settings at all.\n */\nexport function classifyCommand(entry: CommandEntry, evidence: ManifestEvidence): CommandSafety {\n const reasons: string[] = [];\n const services = evidence.services;\n const text = `${entry.command ?? ''} ${evidence.scriptBody ?? ''}`;\n if (entry.mutates) return { safety: 'blocked', reasons: ['it modifies project files; DevWorkspace never runs formatters or fixers'], services };\n if (entry.id === 'migrations') return { safety: 'blocked', reasons: ['it changes a database schema; DevWorkspace never runs migrations'], services };\n if (DESTRUCTIVE.test(text)) return { safety: 'blocked', reasons: ['its definition mentions a destructive or schema-changing operation (drop/reset/seed/migrate/flush)'], services };\n if (SERVICE_CHECKS.has(entry.id)) {\n reasons.push(`${entry.id} tests exercise real services${services.length ? ` (the package uses ${services.join(', ')})` : ''}`);\n } else if (entry.id === 'unit' && services.length > 0 && /(?:^|\\s)(?:run\\s+)?test(?:\\s|$)/.test(entry.command ?? '') && !/unit/.test(entry.command ?? '')) {\n reasons.push(`the catch-all test script of a package that uses ${services.join(', ')} may include integration tests`);\n }\n if (reasons.length === 0) return { safety: 'safe', reasons: [], services };\n if (evidence.hasCredentialFile) reasons.push('a credential file exists in the package (never read), so the database target cannot be verified');\n else reasons.push('the database target cannot be verified');\n return { safety: 'needs-test-environment', reasons, services };\n}\n\n// ---------- explicit test-environment declarations (machine-local, never committed) ----------\n\nexport interface TestEnvDeclaration {\n /** Package directory relative to the project root; \".\" for the root. */\n package: string;\n checks: string[];\n /** Human description of the target, e.g. \"docker compose test stack on this laptop\". Never a connection string. */\n target: string;\n declaredAt: string;\n}\n\ninterface TestEnvFile {\n schemaVersion: 1;\n declarations: TestEnvDeclaration[];\n}\n\nconst file = (ws: Ws): string => path.join(ws.localDir, 'test-environment.json');\n\nexport async function loadTestEnv(ws: Ws): Promise<TestEnvDeclaration[]> {\n if (!(await pathExists(file(ws)))) return [];\n const raw = (await readJsonFile(file(ws))) as Partial<TestEnvFile>;\n return Array.isArray(raw.declarations) ? raw.declarations : [];\n}\n\nexport function assertSafeTarget(target: string): void {\n const t = target.trim();\n if (t.length < 5 || t.length > 200) throw new DevWorkspaceError('USAGE', '--target must be a short description (5-200 characters) of the test environment.');\n if (containsSecret(t) || /[a-z][a-z0-9+.-]*:\\/\\/\\S+/i.test(t) || /(?:password|passwd|pwd|token|secret)\\s*[=:]/i.test(t)) {\n throw new DevWorkspaceError('SECURITY', 'The target description looks like a connection string or credential; refusing to store it.', {\n hint: 'Describe the environment in words (for example \"disposable docker compose stack\"), not how to connect to it.',\n });\n }\n}\n\nexport async function saveDeclaration(ws: Ws, d: TestEnvDeclaration): Promise<void> {\n const all = (await loadTestEnv(ws)).filter((x) => x.package !== d.package);\n await atomicWriteJson(file(ws), { schemaVersion: 1, declarations: [...all, d] } satisfies TestEnvFile);\n}\n\nexport async function removeDeclaration(ws: Ws, pkg: string): Promise<boolean> {\n const all = await loadTestEnv(ws);\n const rest = all.filter((x) => x.package !== pkg);\n if (rest.length === all.length) return false;\n await atomicWriteJson(file(ws), { schemaVersion: 1, declarations: rest } satisfies TestEnvFile);\n return true;\n}\n\nexport function isDeclared(decls: TestEnvDeclaration[], pkg: string, check: string): TestEnvDeclaration | undefined {\n return decls.find((d) => d.package === pkg && d.checks.includes(check));\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import type { CommandEntry } from '../config/schemas.js';
2
+ import type { TaskScope } from '../tasks/task.js';
3
+ export type ChangeMap = Record<string, string>;
4
+ /** Files that differ from HEAD right now (modified, staged or untracked), project-relative, with a content hash. */
5
+ export declare function dirtyFiles(root: string): Promise<{
6
+ head: string | null;
7
+ files: ChangeMap;
8
+ } | null>;
9
+ /** Snapshot taken when a task is created: what is already modified is not the task's doing. */
10
+ export declare function captureScope(root: string, now: string): Promise<TaskScope>;
11
+ /** Files that changed since the task started, with their current hashes. Null when that cannot be determined. */
12
+ export declare function changedSince(root: string, scope: TaskScope): Promise<ChangeMap | null>;
13
+ /** Directories (relative to the project root) that have their own checks in the contract, longest first. */
14
+ export declare function packageDirs(contract: CommandEntry[]): string[];
15
+ /**
16
+ * Fingerprint of the changes inside each package. A change under a package belongs to it; a change outside every package
17
+ * (root config, shared tooling) belongs to all of them. "." covers everything. Returns null when nothing is known.
18
+ */
19
+ export declare function scopeByPackage(changes: ChangeMap | null, contract: CommandEntry[]): Record<string, string> | null;
@@ -0,0 +1,119 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { gitAvailable, parsePorcelainZ } from '../git/git.js';
4
+ import { isSensitiveFile } from '../security/files.js';
5
+ import { sha256File, sha256Text } from '../utils/fs.js';
6
+ import { run } from '../utils/proc.js';
7
+ const MAX_TRACKED_FILES = 2000;
8
+ const MAX_HASH_BYTES = 8 * 1024 * 1024;
9
+ /** Documentation and tool-managed folders never decide which package's checks apply. */
10
+ const NOT_CODE = /^(?:\.devworkspace|\.claude|graphify-out)\/|\.(?:md|mdx|rst|txt)$/i;
11
+ async function git(root, args) {
12
+ const r = await run('git', args, { cwd: root, timeoutMs: 30_000 });
13
+ return r.ok ? r.stdout : null;
14
+ }
15
+ async function fileFingerprint(root, rel) {
16
+ const full = path.join(root, ...rel.split('/'));
17
+ try {
18
+ const st = await fs.lstat(full);
19
+ if (!st.isFile())
20
+ return 'not-a-file';
21
+ if (st.size > MAX_HASH_BYTES)
22
+ return `large:${st.size}:${Math.floor(st.mtimeMs)}`;
23
+ return await sha256File(full);
24
+ }
25
+ catch {
26
+ return 'deleted';
27
+ }
28
+ }
29
+ /** Files that differ from HEAD right now (modified, staged or untracked), project-relative, with a content hash. */
30
+ export async function dirtyFiles(root) {
31
+ if (!(await gitAvailable()))
32
+ return null;
33
+ if ((await git(root, ['rev-parse', '--is-inside-work-tree']))?.trim() !== 'true')
34
+ return null;
35
+ const prefix = ((await git(root, ['rev-parse', '--show-prefix'])) ?? '').trim();
36
+ const head = ((await git(root, ['rev-parse', 'HEAD'])) ?? '').trim() || null;
37
+ const status = await git(root, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
38
+ if (status === null)
39
+ return null;
40
+ const files = {};
41
+ for (const e of parsePorcelainZ(status)) {
42
+ if (e.xy === '!!' || !e.path.startsWith(prefix))
43
+ continue;
44
+ const rel = e.path.slice(prefix.length);
45
+ if (NOT_CODE.test(rel) || isSensitiveFile(rel))
46
+ continue;
47
+ if (Object.keys(files).length >= MAX_TRACKED_FILES)
48
+ break;
49
+ files[rel] = await fileFingerprint(root, rel);
50
+ }
51
+ return { head, files };
52
+ }
53
+ /** Snapshot taken when a task is created: what is already modified is not the task's doing. */
54
+ export async function captureScope(root, now) {
55
+ const d = await dirtyFiles(root);
56
+ return { capturedAt: now, head: d?.head ?? null, baseline: d?.files ?? {} };
57
+ }
58
+ /** Project-relative files committed since `head` (so committing during a task does not hide its changes). */
59
+ async function committedSince(root, head) {
60
+ const prefix = ((await git(root, ['rev-parse', '--show-prefix'])) ?? '').trim();
61
+ const out = await git(root, ['diff', '--name-only', '-z', head, 'HEAD']);
62
+ if (out === null)
63
+ return null;
64
+ return out.split('\0').filter((f) => f && f.startsWith(prefix)).map((f) => f.slice(prefix.length));
65
+ }
66
+ /** Files that changed since the task started, with their current hashes. Null when that cannot be determined. */
67
+ export async function changedSince(root, scope) {
68
+ const now = await dirtyFiles(root);
69
+ if (!now)
70
+ return null;
71
+ const changed = {};
72
+ for (const [rel, hash] of Object.entries(now.files))
73
+ if (scope.baseline[rel] !== hash)
74
+ changed[rel] = hash;
75
+ // A file that was modified at the start and has since been cleaned up (committed or reverted) also changed.
76
+ for (const rel of Object.keys(scope.baseline))
77
+ if (!(rel in now.files))
78
+ changed[rel] = await fileFingerprint(root, rel);
79
+ if (scope.head && now.head && scope.head !== now.head) {
80
+ const committed = await committedSince(root, scope.head);
81
+ if (committed === null)
82
+ return null;
83
+ for (const rel of committed)
84
+ if (!NOT_CODE.test(rel) && !isSensitiveFile(rel) && !(rel in changed))
85
+ changed[rel] = await fileFingerprint(root, rel);
86
+ }
87
+ return changed;
88
+ }
89
+ /** Directories (relative to the project root) that have their own checks in the contract, longest first. */
90
+ export function packageDirs(contract) {
91
+ const dirs = new Set();
92
+ for (const c of contract)
93
+ if (c.available && c.cwd && c.cwd !== '.')
94
+ dirs.add(c.cwd);
95
+ return [...dirs].sort((a, b) => b.length - a.length || a.localeCompare(b));
96
+ }
97
+ /**
98
+ * Fingerprint of the changes inside each package. A change under a package belongs to it; a change outside every package
99
+ * (root config, shared tooling) belongs to all of them. "." covers everything. Returns null when nothing is known.
100
+ */
101
+ export function scopeByPackage(changes, contract) {
102
+ if (!changes)
103
+ return null;
104
+ const files = Object.keys(changes).filter((f) => !NOT_CODE.test(f)).sort();
105
+ if (files.length === 0)
106
+ return null;
107
+ const dirs = packageDirs(contract);
108
+ const owner = (f) => dirs.find((d) => f === d || f.startsWith(`${d}/`)) ?? null;
109
+ const shared = files.filter((f) => owner(f) === null);
110
+ const line = (f) => `${f}:${changes[f]}`;
111
+ const out = { '.': sha256Text(files.map(line).join('\n')) };
112
+ for (const d of dirs) {
113
+ const mine = files.filter((f) => owner(f) === d);
114
+ if (mine.length > 0 || shared.length > 0)
115
+ out[d] = sha256Text([...mine, ...shared].sort().map(line).join('\n'));
116
+ }
117
+ return out;
118
+ }
119
+ //# sourceMappingURL=scope.js.map