@xopcai/xopc 0.0.85 → 0.0.86

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 (60) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/{agents-D3_-kNlZ.js → agents-mS3_HpRI.js} +10 -10
  4. package/dist/gateway/static/root/assets/{apps-page-D7v7649T.js → apps-page-DrfytjOb.js} +1 -1
  5. package/dist/gateway/static/root/assets/{channels-settings-nCaMb0a7.js → channels-settings-BG6b9KrW.js} +1 -1
  6. package/dist/gateway/static/root/assets/{channels-status-swr-C1gZBcJV.js → channels-status-swr-Bs5kMCMI.js} +1 -1
  7. package/dist/gateway/static/root/assets/{cron-api-CoYK0hlm.js → cron-api-BuVcZ5zR.js} +1 -1
  8. package/dist/gateway/static/root/assets/{cron-page-DeGo-Vjc.js → cron-page-BMrloeFH.js} +1 -1
  9. package/dist/gateway/static/root/assets/{dist-DaK4dsss.js → dist-CKU1OOTf.js} +1 -1
  10. package/dist/gateway/static/root/assets/{extension-debug-page-BZngZWbO.js → extension-debug-page-BdW_46sN.js} +1 -1
  11. package/dist/gateway/static/root/assets/{extension-page-D6JSyV27.js → extension-page-DW47KI82.js} +1 -1
  12. package/dist/gateway/static/root/assets/extension-settings-page-B-W4x2xP.js +1 -0
  13. package/dist/gateway/static/root/assets/{field-primitives-Zzl22MvN.js → field-primitives-DPG-oJmx.js} +1 -1
  14. package/dist/gateway/static/root/assets/{heartbeat-config-api-BtIcpG0O.js → heartbeat-config-api-C8dNts9i.js} +1 -1
  15. package/dist/gateway/static/root/assets/{index-D4vM3-P7.js → index-BmVYculr.js} +20 -20
  16. package/dist/gateway/static/root/assets/{logs-page-_d4UJ-qQ.js → logs-page-sTsVWz0X.js} +1 -1
  17. package/dist/gateway/static/root/assets/{sessions-page-5N4aF2Wk.js → sessions-page-FaG_Vlkb.js} +1 -1
  18. package/dist/gateway/static/root/assets/settings-form-section-DuvRQW--.js +1 -0
  19. package/dist/gateway/static/root/assets/settings-page-Bet1OerL.js +3 -0
  20. package/dist/gateway/static/root/assets/{share-preview-page-D4EG_vM1.js → share-preview-page-BtG2kLDh.js} +1 -1
  21. package/dist/gateway/static/root/assets/{skills-page-sPAXhh8w.js → skills-page-DhUO235y.js} +1 -1
  22. package/dist/gateway/static/root/assets/{utils-CYO9eTCM.js → utils-BY7bU1DT.js} +1 -1
  23. package/dist/gateway/static/root/assets/{voice-api-key-field-Ds51havm.js → voice-api-key-field-CGEydndO.js} +1 -1
  24. package/dist/gateway/static/root/index.html +1 -1
  25. package/dist/package.js +1 -1
  26. package/dist/src/agent/tools/workflow-tool.js +3 -3
  27. package/dist/src/agent/tools/workflow-tool.js.map +1 -1
  28. package/dist/src/agent/workflow/builtins/audit-repo.d.ts +5 -1
  29. package/dist/src/agent/workflow/builtins/audit-repo.js +52 -11
  30. package/dist/src/agent/workflow/builtins/audit-repo.js.map +1 -1
  31. package/dist/src/agent/workflow/builtins/debug-incident.d.ts +13 -0
  32. package/dist/src/agent/workflow/builtins/debug-incident.js +155 -0
  33. package/dist/src/agent/workflow/builtins/debug-incident.js.map +1 -0
  34. package/dist/src/agent/workflow/builtins/index.d.ts +3 -1
  35. package/dist/src/agent/workflow/builtins/index.js +11 -1
  36. package/dist/src/agent/workflow/builtins/index.js.map +1 -1
  37. package/dist/src/agent/workflow/builtins/multi-perspective-review.d.ts +6 -1
  38. package/dist/src/agent/workflow/builtins/multi-perspective-review.js +66 -30
  39. package/dist/src/agent/workflow/builtins/multi-perspective-review.js.map +1 -1
  40. package/dist/src/agent/workflow/builtins/pr-review.d.ts +12 -0
  41. package/dist/src/agent/workflow/builtins/pr-review.js +156 -0
  42. package/dist/src/agent/workflow/builtins/pr-review.js.map +1 -0
  43. package/dist/src/agent/workflow/builtins/research.d.ts +5 -1
  44. package/dist/src/agent/workflow/builtins/research.js +37 -6
  45. package/dist/src/agent/workflow/builtins/research.js.map +1 -1
  46. package/dist/src/agent/workflow/catalog.d.ts +5 -0
  47. package/dist/src/agent/workflow/catalog.js +6 -2
  48. package/dist/src/agent/workflow/catalog.js.map +1 -1
  49. package/dist/src/agent/workflow/index.d.ts +1 -1
  50. package/dist/src/agent/workflow/parser.js +9 -0
  51. package/dist/src/agent/workflow/parser.js.map +1 -1
  52. package/dist/src/agent/workflow/types.d.ts +8 -0
  53. package/dist/src/chat-commands/builtins/workflow.js +7 -2
  54. package/dist/src/chat-commands/builtins/workflow.js.map +1 -1
  55. package/dist/src/gateway/heartbeat/service.js +1 -1
  56. package/dist/src/heartbeat/index.js +1 -1
  57. package/package.json +1 -1
  58. package/dist/gateway/static/root/assets/extension-settings-page-8PZcmWI7.js +0 -1
  59. package/dist/gateway/static/root/assets/settings-form-section-D_tgb8r2.js +0 -1
  60. package/dist/gateway/static/root/assets/settings-page-C18xBt4X.js +0 -3
@@ -6,11 +6,18 @@
6
6
  * several independent perspectives, then asks an adversarial judge to decide
7
7
  * what would actually break in practice. Useful for sanity-checking decisions
8
8
  * before they ship.
9
+ *
10
+ * Args:
11
+ * - target: what to review
12
+ * - lenses: optional array of { name, angle } to override default lenses
13
+ * - skipAdversarial: when true, skip the adversarial judge phase
9
14
  */
10
15
  const MULTI_PERSPECTIVE_REVIEW_SCRIPT = `export const meta = {
11
16
  name: 'multi_perspective_review',
12
17
  description: 'Review a target from N independent perspectives, then adversarially judge what would actually break.',
13
18
  whenToUse: 'User wants a stress-test of a design, plan, PR, or proposal before committing to it.',
19
+ tags: ['review', 'planning', 'decision'],
20
+ estimatedAgents: { min: 5, max: 6 },
14
21
  phases: [
15
22
  { title: 'Lenses' },
16
23
  { title: 'Adversarial' },
@@ -18,20 +25,32 @@ const MULTI_PERSPECTIVE_REVIEW_SCRIPT = `export const meta = {
18
25
  ],
19
26
  }
20
27
 
28
+ const READ_ONLY_TOOLS = ['read_file', 'grep', 'find', 'list_dir']
29
+
21
30
  const target = args && typeof args === 'object' && args.target
22
31
  ? String(args.target)
23
32
  : 'No explicit target was provided. Treat the currently focused file or recent context as the target.'
24
33
 
25
- const LENSES = [
26
- { name: 'User', angle: 'How a real user experiences this. Friction, confusion, surprise paths, accessibility.' },
27
- { name: 'Operator', angle: 'How an on-call engineer experiences this in production. Failure modes, observability, rollback.' },
28
- { name: 'Skeptic', angle: 'Hidden assumptions. What is being implied but not stated. What would break under load or weird input.' },
29
- { name: 'Maintainer', angle: 'Six-month-later view. Clarity, naming, layering, ease of changing nearby code.' },
34
+ const skipAdversarial = Boolean(args && typeof args === 'object' && args.skipAdversarial)
35
+
36
+ const DEFAULT_LENSES = [
37
+ { name: 'User', angle: 'How a real user experiences this. Friction, confusion, surprise paths, accessibility.' },
38
+ { name: 'Operator', angle: 'How an on-call engineer experiences this in production. Failure modes, observability, rollback.' },
39
+ { name: 'Skeptic', angle: 'Hidden assumptions. What is being implied but not stated. What would break under load or weird input.' },
40
+ { name: 'Maintainer', angle: 'Six-month-later view. Clarity, naming, layering, ease of changing nearby code.' },
30
41
  ]
31
42
 
43
+ let lenses = DEFAULT_LENSES
44
+ if (args && typeof args === 'object' && Array.isArray(args.lenses) && args.lenses.length) {
45
+ lenses = args.lenses
46
+ .filter((l) => l && typeof l === 'object' && l.name && l.angle)
47
+ .map((l) => ({ name: String(l.name), angle: String(l.angle) }))
48
+ if (!lenses.length) lenses = DEFAULT_LENSES
49
+ }
50
+
32
51
  phase('Lenses')
33
52
  const lensViews = await parallel(
34
- LENSES.map((l) => () =>
53
+ lenses.map((l) => () =>
35
54
  agent(
36
55
  'Review the following target through the ' + l.name + ' lens.\\n' +
37
56
  'Lens focus: ' + l.angle + '\\n\\n' +
@@ -39,6 +58,7 @@ const lensViews = await parallel(
39
58
  'Return 3–7 concrete observations. Each entry: title (5–10 words), why-it-matters (1 sentence), risk (low/med/high).',
40
59
  {
41
60
  label: l.name + ' lens',
61
+ toolset: READ_ONLY_TOOLS,
42
62
  schema: {
43
63
  type: 'object',
44
64
  properties: {
@@ -62,47 +82,63 @@ const lensViews = await parallel(
62
82
  ),
63
83
  )
64
84
 
65
- phase('Adversarial')
66
85
  const valid = lensViews.filter(Boolean)
67
86
  const allObs = valid.flatMap((v, i) =>
68
- (v?.observations ?? []).map((o) => ({ lens: LENSES[i].name, ...o })),
87
+ (v?.observations ?? []).map((o) => ({ lens: lenses[i].name, ...o })),
69
88
  )
70
89
 
71
- const verdict = await agent(
72
- 'You are an adversarial judge. Given these multi-lens observations of a target, decide which would actually cause real harm if shipped as-is. ' +
73
- 'Default to refuted=true unless an observation has clear, mechanism-level evidence.\\n\\n' +
74
- JSON.stringify(allObs, null, 2),
75
- {
76
- label: 'adversarial verdict',
77
- schema: {
78
- type: 'object',
79
- properties: {
80
- verdicts: {
81
- type: 'array',
82
- items: {
83
- type: 'object',
84
- properties: {
85
- title: { type: 'string' },
86
- lens: { type: 'string' },
87
- realRisk: { type: 'boolean' },
88
- reason: { type: 'string' },
90
+ let verdict = null
91
+ if (!skipAdversarial) {
92
+ phase('Adversarial')
93
+ verdict = await agent(
94
+ 'You are an adversarial judge. Given these multi-lens observations of a target, decide which would actually cause real harm if shipped as-is. ' +
95
+ 'Default to realRisk=false unless an observation has clear, mechanism-level evidence. ' +
96
+ 'Rate evidenceStrength as weak | moderate | strong.\\n\\n' +
97
+ JSON.stringify(allObs, null, 2),
98
+ {
99
+ label: 'adversarial verdict',
100
+ schema: {
101
+ type: 'object',
102
+ properties: {
103
+ verdicts: {
104
+ type: 'array',
105
+ items: {
106
+ type: 'object',
107
+ properties: {
108
+ title: { type: 'string' },
109
+ lens: { type: 'string' },
110
+ realRisk: { type: 'boolean' },
111
+ evidenceStrength: { type: 'string', enum: ['weak', 'moderate', 'strong'] },
112
+ reason: { type: 'string' },
113
+ },
114
+ required: ['title', 'lens', 'realRisk', 'evidenceStrength', 'reason'],
89
115
  },
90
- required: ['title', 'lens', 'realRisk', 'reason'],
91
116
  },
92
117
  },
118
+ required: ['verdicts'],
93
119
  },
94
- required: ['verdicts'],
95
120
  },
96
- },
97
- )
121
+ )
122
+ }
98
123
 
99
124
  phase('Synthesize')
100
125
  const confirmed = (verdict?.verdicts ?? []).filter((v) => v.realRisk)
126
+ const goNoGo = skipAdversarial
127
+ ? (allObs.some((o) => o.risk === 'high') ? 'fix_first' : 'ship')
128
+ : confirmed.some((v) => v.evidenceStrength === 'strong')
129
+ ? 'fix_first'
130
+ : confirmed.length
131
+ ? 'fix_first'
132
+ : 'ship'
133
+
101
134
  return {
102
135
  ok: true,
103
136
  target,
137
+ lenses: lenses.map((l) => l.name),
138
+ skipAdversarial,
104
139
  observationCount: allObs.length,
105
140
  confirmedRiskCount: confirmed.length,
141
+ goNoGo,
106
142
  topRisks: confirmed.slice(0, 10),
107
143
  allVerdicts: verdict?.verdicts ?? [],
108
144
  }
@@ -1 +1 @@
1
- {"version":3,"file":"multi-perspective-review.js","names":[],"sources":["../../../../../src/agent/workflow/builtins/multi-perspective-review.ts"],"sourcesContent":["/**\n * Built-in workflow: `multi_perspective_review`\n *\n * Reviews a target (file, PR, design doc, plan — passed via args.target) from\n * several independent perspectives, then asks an adversarial judge to decide\n * what would actually break in practice. Useful for sanity-checking decisions\n * before they ship.\n */\n\nexport const MULTI_PERSPECTIVE_REVIEW_SCRIPT = `export const meta = {\n name: 'multi_perspective_review',\n description: 'Review a target from N independent perspectives, then adversarially judge what would actually break.',\n whenToUse: 'User wants a stress-test of a design, plan, PR, or proposal before committing to it.',\n phases: [\n { title: 'Lenses' },\n { title: 'Adversarial' },\n { title: 'Synthesize' },\n ],\n}\n\nconst target = args && typeof args === 'object' && args.target\n ? String(args.target)\n : 'No explicit target was provided. Treat the currently focused file or recent context as the target.'\n\nconst LENSES = [\n { name: 'User', angle: 'How a real user experiences this. Friction, confusion, surprise paths, accessibility.' },\n { name: 'Operator', angle: 'How an on-call engineer experiences this in production. Failure modes, observability, rollback.' },\n { name: 'Skeptic', angle: 'Hidden assumptions. What is being implied but not stated. What would break under load or weird input.' },\n { name: 'Maintainer', angle: 'Six-month-later view. Clarity, naming, layering, ease of changing nearby code.' },\n]\n\nphase('Lenses')\nconst lensViews = await parallel(\n LENSES.map((l) => () =>\n agent(\n 'Review the following target through the ' + l.name + ' lens.\\\\n' +\n 'Lens focus: ' + l.angle + '\\\\n\\\\n' +\n 'TARGET:\\\\n' + target + '\\\\n\\\\n' +\n 'Return 3–7 concrete observations. Each entry: title (5–10 words), why-it-matters (1 sentence), risk (low/med/high).',\n {\n label: l.name + ' lens',\n schema: {\n type: 'object',\n properties: {\n observations: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n why: { type: 'string' },\n risk: { type: 'string', enum: ['low', 'med', 'high'] },\n },\n required: ['title', 'why', 'risk'],\n },\n },\n },\n required: ['observations'],\n },\n },\n ),\n ),\n)\n\nphase('Adversarial')\nconst valid = lensViews.filter(Boolean)\nconst allObs = valid.flatMap((v, i) =>\n (v?.observations ?? []).map((o) => ({ lens: LENSES[i].name, ...o })),\n)\n\nconst verdict = await agent(\n 'You are an adversarial judge. Given these multi-lens observations of a target, decide which would actually cause real harm if shipped as-is. ' +\n 'Default to refuted=true unless an observation has clear, mechanism-level evidence.\\\\n\\\\n' +\n JSON.stringify(allObs, null, 2),\n {\n label: 'adversarial verdict',\n schema: {\n type: 'object',\n properties: {\n verdicts: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n lens: { type: 'string' },\n realRisk: { type: 'boolean' },\n reason: { type: 'string' },\n },\n required: ['title', 'lens', 'realRisk', 'reason'],\n },\n },\n },\n required: ['verdicts'],\n },\n },\n)\n\nphase('Synthesize')\nconst confirmed = (verdict?.verdicts ?? []).filter((v) => v.realRisk)\nreturn {\n ok: true,\n target,\n observationCount: allObs.length,\n confirmedRiskCount: confirmed.length,\n topRisks: confirmed.slice(0, 10),\n allVerdicts: verdict?.verdicts ?? [],\n}\n`\n"],"mappings":";;;;;;;;;AASA,MAAa,kCAAkC"}
1
+ {"version":3,"file":"multi-perspective-review.js","names":[],"sources":["../../../../../src/agent/workflow/builtins/multi-perspective-review.ts"],"sourcesContent":["/**\n * Built-in workflow: `multi_perspective_review`\n *\n * Reviews a target (file, PR, design doc, plan — passed via args.target) from\n * several independent perspectives, then asks an adversarial judge to decide\n * what would actually break in practice. Useful for sanity-checking decisions\n * before they ship.\n *\n * Args:\n * - target: what to review\n * - lenses: optional array of { name, angle } to override default lenses\n * - skipAdversarial: when true, skip the adversarial judge phase\n */\n\nexport const MULTI_PERSPECTIVE_REVIEW_SCRIPT = `export const meta = {\n name: 'multi_perspective_review',\n description: 'Review a target from N independent perspectives, then adversarially judge what would actually break.',\n whenToUse: 'User wants a stress-test of a design, plan, PR, or proposal before committing to it.',\n tags: ['review', 'planning', 'decision'],\n estimatedAgents: { min: 5, max: 6 },\n phases: [\n { title: 'Lenses' },\n { title: 'Adversarial' },\n { title: 'Synthesize' },\n ],\n}\n\nconst READ_ONLY_TOOLS = ['read_file', 'grep', 'find', 'list_dir']\n\nconst target = args && typeof args === 'object' && args.target\n ? String(args.target)\n : 'No explicit target was provided. Treat the currently focused file or recent context as the target.'\n\nconst skipAdversarial = Boolean(args && typeof args === 'object' && args.skipAdversarial)\n\nconst DEFAULT_LENSES = [\n { name: 'User', angle: 'How a real user experiences this. Friction, confusion, surprise paths, accessibility.' },\n { name: 'Operator', angle: 'How an on-call engineer experiences this in production. Failure modes, observability, rollback.' },\n { name: 'Skeptic', angle: 'Hidden assumptions. What is being implied but not stated. What would break under load or weird input.' },\n { name: 'Maintainer', angle: 'Six-month-later view. Clarity, naming, layering, ease of changing nearby code.' },\n]\n\nlet lenses = DEFAULT_LENSES\nif (args && typeof args === 'object' && Array.isArray(args.lenses) && args.lenses.length) {\n lenses = args.lenses\n .filter((l) => l && typeof l === 'object' && l.name && l.angle)\n .map((l) => ({ name: String(l.name), angle: String(l.angle) }))\n if (!lenses.length) lenses = DEFAULT_LENSES\n}\n\nphase('Lenses')\nconst lensViews = await parallel(\n lenses.map((l) => () =>\n agent(\n 'Review the following target through the ' + l.name + ' lens.\\\\n' +\n 'Lens focus: ' + l.angle + '\\\\n\\\\n' +\n 'TARGET:\\\\n' + target + '\\\\n\\\\n' +\n 'Return 3–7 concrete observations. Each entry: title (5–10 words), why-it-matters (1 sentence), risk (low/med/high).',\n {\n label: l.name + ' lens',\n toolset: READ_ONLY_TOOLS,\n schema: {\n type: 'object',\n properties: {\n observations: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n why: { type: 'string' },\n risk: { type: 'string', enum: ['low', 'med', 'high'] },\n },\n required: ['title', 'why', 'risk'],\n },\n },\n },\n required: ['observations'],\n },\n },\n ),\n ),\n)\n\nconst valid = lensViews.filter(Boolean)\nconst allObs = valid.flatMap((v, i) =>\n (v?.observations ?? []).map((o) => ({ lens: lenses[i].name, ...o })),\n)\n\nlet verdict = null\nif (!skipAdversarial) {\n phase('Adversarial')\n verdict = await agent(\n 'You are an adversarial judge. Given these multi-lens observations of a target, decide which would actually cause real harm if shipped as-is. ' +\n 'Default to realRisk=false unless an observation has clear, mechanism-level evidence. ' +\n 'Rate evidenceStrength as weak | moderate | strong.\\\\n\\\\n' +\n JSON.stringify(allObs, null, 2),\n {\n label: 'adversarial verdict',\n schema: {\n type: 'object',\n properties: {\n verdicts: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n lens: { type: 'string' },\n realRisk: { type: 'boolean' },\n evidenceStrength: { type: 'string', enum: ['weak', 'moderate', 'strong'] },\n reason: { type: 'string' },\n },\n required: ['title', 'lens', 'realRisk', 'evidenceStrength', 'reason'],\n },\n },\n },\n required: ['verdicts'],\n },\n },\n )\n}\n\nphase('Synthesize')\nconst confirmed = (verdict?.verdicts ?? []).filter((v) => v.realRisk)\nconst goNoGo = skipAdversarial\n ? (allObs.some((o) => o.risk === 'high') ? 'fix_first' : 'ship')\n : confirmed.some((v) => v.evidenceStrength === 'strong')\n ? 'fix_first'\n : confirmed.length\n ? 'fix_first'\n : 'ship'\n\nreturn {\n ok: true,\n target,\n lenses: lenses.map((l) => l.name),\n skipAdversarial,\n observationCount: allObs.length,\n confirmedRiskCount: confirmed.length,\n goNoGo,\n topRisks: confirmed.slice(0, 10),\n allVerdicts: verdict?.verdicts ?? [],\n}\n`\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAa,kCAAkC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Built-in workflow: `pr_review`
3
+ *
4
+ * Focused review of a PR, diff, or commit range — lighter than full repo audit.
5
+ * Parallel reviewers cover correctness, security, tests, API compat, and perf;
6
+ * final phase produces a ship/block verdict with blockers vs suggestions.
7
+ *
8
+ * Args:
9
+ * - target: PR description, diff, commit range, or file list
10
+ * - diff: alias for target
11
+ */
12
+ export declare const PR_REVIEW_SCRIPT = "export const meta = {\n name: 'pr_review',\n description: 'Review a PR/diff/commit range with parallel focused reviewers and a ship/block verdict.',\n whenToUse: 'User asks to review a PR, diff, specific changes, or commit range (not the whole repo).',\n tags: ['code-review', 'pr'],\n estimatedAgents: { min: 7, max: 7 },\n phases: [\n { title: 'Scope' },\n { title: 'Review' },\n { title: 'Verdict' },\n ],\n}\n\nconst READ_ONLY_TOOLS = ['read_file', 'grep', 'find', 'list_dir', 'shell']\n\nconst target = args && typeof args === 'object'\n ? String(args.target ?? args.diff ?? '')\n : ''\nconst reviewTarget = target.trim()\n ? target\n : 'Recent changes in the working tree or the diff/context from the current conversation.'\n\nconst REVIEWERS = [\n { key: 'correctness', focus: 'Logic bugs, edge cases, error handling, null safety, race conditions in changed code.' },\n { key: 'security', focus: 'Auth/authz regressions, input validation, secret exposure, injection, unsafe deserialization in the diff.' },\n { key: 'tests', focus: 'Missing tests for changed behavior, brittle assertions, untested edge cases introduced by this change.' },\n { key: 'api_compat', focus: 'Breaking public API changes, schema migrations, backward compatibility, deprecation handling.' },\n { key: 'perf', focus: 'Regressions in hot paths, accidental N+1, sync I/O, unbounded loops or allocations in changed code.' },\n]\n\nphase('Scope')\nconst scope = await agent(\n 'Identify what changed for this review target. List changed files, blast radius (what depends on them), and the apparent intent of the change. Be concise.\\n\\nTARGET:\\n' +\n reviewTarget,\n {\n label: 'change scope',\n toolset: READ_ONLY_TOOLS,\n schema: {\n type: 'object',\n properties: {\n changedFiles: { type: 'array', items: { type: 'string' } },\n intent: { type: 'string' },\n blastRadius: { type: 'string' },\n },\n required: ['changedFiles', 'intent'],\n },\n },\n)\n\nphase('Review')\nconst reviews = await parallel(\n REVIEWERS.map((r) => () =>\n agent(\n 'Review these changes through the ' + r.key + ' lens.\\n' +\n 'Focus: ' + r.focus + '\\n\\n' +\n 'TARGET:\\n' + reviewTarget + '\\n\\n' +\n 'SCOPE:\\n' + JSON.stringify(scope, null, 2) + '\\n\\n' +\n 'Return findings: file, severity (low/med/high/blocker), title, fix suggestion. Blocker = must fix before merge.',\n {\n label: r.key,\n toolset: READ_ONLY_TOOLS,\n schema: {\n type: 'object',\n properties: {\n findings: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n file: { type: 'string' },\n severity: { type: 'string', enum: ['low', 'med', 'high', 'blocker'] },\n title: { type: 'string' },\n fix: { type: 'string' },\n },\n required: ['file', 'severity', 'title', 'fix'],\n },\n },\n },\n required: ['findings'],\n },\n },\n ),\n ),\n)\n\nphase('Verdict')\nconst live = reviews.filter(Boolean)\nconst byReviewer = {}\nfor (let i = 0; i < REVIEWERS.length; i++) {\n byReviewer[REVIEWERS[i].key] = live[i]?.findings ?? []\n}\n\nconst verdict = await agent(\n 'Synthesize a PR review verdict. Separate blockers from suggestions. Deduplicate. Recommend ship | fix_first | block.\\n\\n' +\n JSON.stringify(byReviewer, null, 2),\n {\n label: 'verdict',\n schema: {\n type: 'object',\n properties: {\n recommendation: { type: 'string', enum: ['ship', 'fix_first', 'block'] },\n summary: { type: 'string' },\n blockers: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n file: { type: 'string' },\n title: { type: 'string' },\n fix: { type: 'string' },\n },\n required: ['file', 'title'],\n },\n },\n suggestions: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n file: { type: 'string' },\n severity: { type: 'string' },\n title: { type: 'string' },\n },\n required: ['file', 'title'],\n },\n },\n },\n required: ['recommendation', 'summary', 'blockers'],\n },\n },\n)\n\nreturn {\n ok: true,\n target: reviewTarget,\n scope,\n ...(verdict ?? { recommendation: 'fix_first', summary: 'verdict failed', blockers: [], suggestions: [] }),\n byReviewer,\n}\n";
@@ -0,0 +1,156 @@
1
+ //#region src/agent/workflow/builtins/pr-review.ts
2
+ /**
3
+ * Built-in workflow: `pr_review`
4
+ *
5
+ * Focused review of a PR, diff, or commit range — lighter than full repo audit.
6
+ * Parallel reviewers cover correctness, security, tests, API compat, and perf;
7
+ * final phase produces a ship/block verdict with blockers vs suggestions.
8
+ *
9
+ * Args:
10
+ * - target: PR description, diff, commit range, or file list
11
+ * - diff: alias for target
12
+ */
13
+ const PR_REVIEW_SCRIPT = `export const meta = {
14
+ name: 'pr_review',
15
+ description: 'Review a PR/diff/commit range with parallel focused reviewers and a ship/block verdict.',
16
+ whenToUse: 'User asks to review a PR, diff, specific changes, or commit range (not the whole repo).',
17
+ tags: ['code-review', 'pr'],
18
+ estimatedAgents: { min: 7, max: 7 },
19
+ phases: [
20
+ { title: 'Scope' },
21
+ { title: 'Review' },
22
+ { title: 'Verdict' },
23
+ ],
24
+ }
25
+
26
+ const READ_ONLY_TOOLS = ['read_file', 'grep', 'find', 'list_dir', 'shell']
27
+
28
+ const target = args && typeof args === 'object'
29
+ ? String(args.target ?? args.diff ?? '')
30
+ : ''
31
+ const reviewTarget = target.trim()
32
+ ? target
33
+ : 'Recent changes in the working tree or the diff/context from the current conversation.'
34
+
35
+ const REVIEWERS = [
36
+ { key: 'correctness', focus: 'Logic bugs, edge cases, error handling, null safety, race conditions in changed code.' },
37
+ { key: 'security', focus: 'Auth/authz regressions, input validation, secret exposure, injection, unsafe deserialization in the diff.' },
38
+ { key: 'tests', focus: 'Missing tests for changed behavior, brittle assertions, untested edge cases introduced by this change.' },
39
+ { key: 'api_compat', focus: 'Breaking public API changes, schema migrations, backward compatibility, deprecation handling.' },
40
+ { key: 'perf', focus: 'Regressions in hot paths, accidental N+1, sync I/O, unbounded loops or allocations in changed code.' },
41
+ ]
42
+
43
+ phase('Scope')
44
+ const scope = await agent(
45
+ 'Identify what changed for this review target. List changed files, blast radius (what depends on them), and the apparent intent of the change. Be concise.\\n\\nTARGET:\\n' +
46
+ reviewTarget,
47
+ {
48
+ label: 'change scope',
49
+ toolset: READ_ONLY_TOOLS,
50
+ schema: {
51
+ type: 'object',
52
+ properties: {
53
+ changedFiles: { type: 'array', items: { type: 'string' } },
54
+ intent: { type: 'string' },
55
+ blastRadius: { type: 'string' },
56
+ },
57
+ required: ['changedFiles', 'intent'],
58
+ },
59
+ },
60
+ )
61
+
62
+ phase('Review')
63
+ const reviews = await parallel(
64
+ REVIEWERS.map((r) => () =>
65
+ agent(
66
+ 'Review these changes through the ' + r.key + ' lens.\\n' +
67
+ 'Focus: ' + r.focus + '\\n\\n' +
68
+ 'TARGET:\\n' + reviewTarget + '\\n\\n' +
69
+ 'SCOPE:\\n' + JSON.stringify(scope, null, 2) + '\\n\\n' +
70
+ 'Return findings: file, severity (low/med/high/blocker), title, fix suggestion. Blocker = must fix before merge.',
71
+ {
72
+ label: r.key,
73
+ toolset: READ_ONLY_TOOLS,
74
+ schema: {
75
+ type: 'object',
76
+ properties: {
77
+ findings: {
78
+ type: 'array',
79
+ items: {
80
+ type: 'object',
81
+ properties: {
82
+ file: { type: 'string' },
83
+ severity: { type: 'string', enum: ['low', 'med', 'high', 'blocker'] },
84
+ title: { type: 'string' },
85
+ fix: { type: 'string' },
86
+ },
87
+ required: ['file', 'severity', 'title', 'fix'],
88
+ },
89
+ },
90
+ },
91
+ required: ['findings'],
92
+ },
93
+ },
94
+ ),
95
+ ),
96
+ )
97
+
98
+ phase('Verdict')
99
+ const live = reviews.filter(Boolean)
100
+ const byReviewer = {}
101
+ for (let i = 0; i < REVIEWERS.length; i++) {
102
+ byReviewer[REVIEWERS[i].key] = live[i]?.findings ?? []
103
+ }
104
+
105
+ const verdict = await agent(
106
+ 'Synthesize a PR review verdict. Separate blockers from suggestions. Deduplicate. Recommend ship | fix_first | block.\\n\\n' +
107
+ JSON.stringify(byReviewer, null, 2),
108
+ {
109
+ label: 'verdict',
110
+ schema: {
111
+ type: 'object',
112
+ properties: {
113
+ recommendation: { type: 'string', enum: ['ship', 'fix_first', 'block'] },
114
+ summary: { type: 'string' },
115
+ blockers: {
116
+ type: 'array',
117
+ items: {
118
+ type: 'object',
119
+ properties: {
120
+ file: { type: 'string' },
121
+ title: { type: 'string' },
122
+ fix: { type: 'string' },
123
+ },
124
+ required: ['file', 'title'],
125
+ },
126
+ },
127
+ suggestions: {
128
+ type: 'array',
129
+ items: {
130
+ type: 'object',
131
+ properties: {
132
+ file: { type: 'string' },
133
+ severity: { type: 'string' },
134
+ title: { type: 'string' },
135
+ },
136
+ required: ['file', 'title'],
137
+ },
138
+ },
139
+ },
140
+ required: ['recommendation', 'summary', 'blockers'],
141
+ },
142
+ },
143
+ )
144
+
145
+ return {
146
+ ok: true,
147
+ target: reviewTarget,
148
+ scope,
149
+ ...(verdict ?? { recommendation: 'fix_first', summary: 'verdict failed', blockers: [], suggestions: [] }),
150
+ byReviewer,
151
+ }
152
+ `;
153
+ //#endregion
154
+ export { PR_REVIEW_SCRIPT };
155
+
156
+ //# sourceMappingURL=pr-review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pr-review.js","names":[],"sources":["../../../../../src/agent/workflow/builtins/pr-review.ts"],"sourcesContent":["/**\n * Built-in workflow: `pr_review`\n *\n * Focused review of a PR, diff, or commit range — lighter than full repo audit.\n * Parallel reviewers cover correctness, security, tests, API compat, and perf;\n * final phase produces a ship/block verdict with blockers vs suggestions.\n *\n * Args:\n * - target: PR description, diff, commit range, or file list\n * - diff: alias for target\n */\n\nexport const PR_REVIEW_SCRIPT = `export const meta = {\n name: 'pr_review',\n description: 'Review a PR/diff/commit range with parallel focused reviewers and a ship/block verdict.',\n whenToUse: 'User asks to review a PR, diff, specific changes, or commit range (not the whole repo).',\n tags: ['code-review', 'pr'],\n estimatedAgents: { min: 7, max: 7 },\n phases: [\n { title: 'Scope' },\n { title: 'Review' },\n { title: 'Verdict' },\n ],\n}\n\nconst READ_ONLY_TOOLS = ['read_file', 'grep', 'find', 'list_dir', 'shell']\n\nconst target = args && typeof args === 'object'\n ? String(args.target ?? args.diff ?? '')\n : ''\nconst reviewTarget = target.trim()\n ? target\n : 'Recent changes in the working tree or the diff/context from the current conversation.'\n\nconst REVIEWERS = [\n { key: 'correctness', focus: 'Logic bugs, edge cases, error handling, null safety, race conditions in changed code.' },\n { key: 'security', focus: 'Auth/authz regressions, input validation, secret exposure, injection, unsafe deserialization in the diff.' },\n { key: 'tests', focus: 'Missing tests for changed behavior, brittle assertions, untested edge cases introduced by this change.' },\n { key: 'api_compat', focus: 'Breaking public API changes, schema migrations, backward compatibility, deprecation handling.' },\n { key: 'perf', focus: 'Regressions in hot paths, accidental N+1, sync I/O, unbounded loops or allocations in changed code.' },\n]\n\nphase('Scope')\nconst scope = await agent(\n 'Identify what changed for this review target. List changed files, blast radius (what depends on them), and the apparent intent of the change. Be concise.\\\\n\\\\nTARGET:\\\\n' +\n reviewTarget,\n {\n label: 'change scope',\n toolset: READ_ONLY_TOOLS,\n schema: {\n type: 'object',\n properties: {\n changedFiles: { type: 'array', items: { type: 'string' } },\n intent: { type: 'string' },\n blastRadius: { type: 'string' },\n },\n required: ['changedFiles', 'intent'],\n },\n },\n)\n\nphase('Review')\nconst reviews = await parallel(\n REVIEWERS.map((r) => () =>\n agent(\n 'Review these changes through the ' + r.key + ' lens.\\\\n' +\n 'Focus: ' + r.focus + '\\\\n\\\\n' +\n 'TARGET:\\\\n' + reviewTarget + '\\\\n\\\\n' +\n 'SCOPE:\\\\n' + JSON.stringify(scope, null, 2) + '\\\\n\\\\n' +\n 'Return findings: file, severity (low/med/high/blocker), title, fix suggestion. Blocker = must fix before merge.',\n {\n label: r.key,\n toolset: READ_ONLY_TOOLS,\n schema: {\n type: 'object',\n properties: {\n findings: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n file: { type: 'string' },\n severity: { type: 'string', enum: ['low', 'med', 'high', 'blocker'] },\n title: { type: 'string' },\n fix: { type: 'string' },\n },\n required: ['file', 'severity', 'title', 'fix'],\n },\n },\n },\n required: ['findings'],\n },\n },\n ),\n ),\n)\n\nphase('Verdict')\nconst live = reviews.filter(Boolean)\nconst byReviewer = {}\nfor (let i = 0; i < REVIEWERS.length; i++) {\n byReviewer[REVIEWERS[i].key] = live[i]?.findings ?? []\n}\n\nconst verdict = await agent(\n 'Synthesize a PR review verdict. Separate blockers from suggestions. Deduplicate. Recommend ship | fix_first | block.\\\\n\\\\n' +\n JSON.stringify(byReviewer, null, 2),\n {\n label: 'verdict',\n schema: {\n type: 'object',\n properties: {\n recommendation: { type: 'string', enum: ['ship', 'fix_first', 'block'] },\n summary: { type: 'string' },\n blockers: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n file: { type: 'string' },\n title: { type: 'string' },\n fix: { type: 'string' },\n },\n required: ['file', 'title'],\n },\n },\n suggestions: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n file: { type: 'string' },\n severity: { type: 'string' },\n title: { type: 'string' },\n },\n required: ['file', 'title'],\n },\n },\n },\n required: ['recommendation', 'summary', 'blockers'],\n },\n },\n)\n\nreturn {\n ok: true,\n target: reviewTarget,\n scope,\n ...(verdict ?? { recommendation: 'fix_first', summary: 'verdict failed', blockers: [], suggestions: [] }),\n byReviewer,\n}\n`\n"],"mappings":";;;;;;;;;;;;AAYA,MAAa,mBAAmB"}
@@ -5,5 +5,9 @@
5
5
  * exploration / source-reading angles in parallel, then synthesises a cited
6
6
  * report. Each angle is its own subagent so source reading does not pollute the
7
7
  * parent context.
8
+ *
9
+ * Args:
10
+ * - question: research question
11
+ * - depth: 'quick' (2 angles) | 'standard' (4) | 'deep' (6)
8
12
  */
9
- export declare const RESEARCH_SCRIPT = "export const meta = {\n name: 'research',\n description: 'Multi-angle research on a question with parallel exploration and a cited synthesis.',\n whenToUse: 'User asks a non-trivial research question that benefits from multiple search angles or source reads.',\n phases: [\n { title: 'Frame' },\n { title: 'Sweep' },\n { title: 'Synthesize' },\n ],\n}\n\nconst question = args && typeof args === 'object' && args.question\n ? String(args.question)\n : 'No explicit question supplied; infer from the most recent user turn.'\n\nphase('Frame')\nconst frame = await agent(\n 'Frame this research question. Return 3\u20135 distinct angles worth investigating, plus the single most decisive sub-question for each. Be concrete.\\n\\nQUESTION:\\n' +\n question,\n {\n label: 'framing',\n schema: {\n type: 'object',\n properties: {\n angles: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n key_question: { type: 'string' },\n },\n required: ['title', 'key_question'],\n },\n },\n },\n required: ['angles'],\n },\n },\n)\n\nif (!frame || !frame.angles?.length) {\n return { ok: false, reason: 'framing failed', question }\n}\n\nphase('Sweep')\nconst angleReports = await parallel(\n frame.angles.map((a) => () =>\n agent(\n 'Investigate this angle. Use search and source-read tools liberally. Distinguish what you can confirm from what is conjecture.\\n\\n' +\n 'ANGLE: ' + a.title + '\\n' +\n 'KEY QUESTION: ' + a.key_question + '\\n\\n' +\n 'Return: 3\u20136 grounded findings (each with a 1-line claim and a source URL or file path), plus the strongest counter-evidence.',\n {\n label: a.title,\n schema: {\n type: 'object',\n properties: {\n findings: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n claim: { type: 'string' },\n source: { type: 'string' },\n confidence: { type: 'string', enum: ['low', 'med', 'high'] },\n },\n required: ['claim', 'source', 'confidence'],\n },\n },\n counterEvidence: { type: 'string' },\n },\n required: ['findings'],\n },\n },\n ),\n ),\n)\n\nphase('Synthesize')\nconst live = angleReports.filter(Boolean)\nconst synthesis = await agent(\n 'Synthesize a cited research report from these angle-level findings. Drop unsupported or duplicate claims. Use the highest-confidence source per claim. ' +\n 'Return: an executive summary (max 5 sentences), a bullet list of top findings with inline source URLs, and one section listing open questions.\\n\\n' +\n 'QUESTION:\\n' + question + '\\n\\n' +\n JSON.stringify({ angles: frame.angles, reports: live }, null, 2),\n {\n label: 'synthesis',\n schema: {\n type: 'object',\n properties: {\n executiveSummary: { type: 'string' },\n topFindings: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n claim: { type: 'string' },\n source: { type: 'string' },\n },\n required: ['claim', 'source'],\n },\n },\n openQuestions: { type: 'array', items: { type: 'string' } },\n },\n required: ['executiveSummary', 'topFindings'],\n },\n },\n)\n\nreturn {\n ok: true,\n question,\n ...(synthesis ?? { executiveSummary: 'synthesis failed', topFindings: [] }),\n}\n";
13
+ export declare const RESEARCH_SCRIPT = "export const meta = {\n name: 'research',\n description: 'Multi-angle research on a question with parallel exploration and a cited synthesis.',\n whenToUse: 'User asks a non-trivial research question that benefits from multiple search angles or source reads.',\n tags: ['research', 'investigation'],\n estimatedAgents: { min: 4, max: 8 },\n phases: [\n { title: 'Frame' },\n { title: 'Sweep' },\n { title: 'Synthesize' },\n ],\n}\n\nconst RESEARCH_TOOLS = ['web_search', 'web_fetch', 'read_file', 'grep', 'find', 'list_dir']\n\nconst question = args && typeof args === 'object' && args.question\n ? String(args.question)\n : 'No explicit question supplied; infer from the most recent user turn.'\n\nconst depth = args && typeof args === 'object' && args.depth\n ? String(args.depth)\n : 'standard'\nconst maxAngles = depth === 'quick' ? 2 : depth === 'deep' ? 6 : 4\n\nphase('Frame')\nconst frame = await agent(\n 'Frame this research question. Return exactly ' + maxAngles + ' distinct angles worth investigating, ' +\n 'each with the single most decisive sub-question. Be concrete.\\n\\nQUESTION:\\n' +\n question,\n {\n label: 'framing',\n schema: {\n type: 'object',\n properties: {\n angles: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n key_question: { type: 'string' },\n },\n required: ['title', 'key_question'],\n },\n },\n },\n required: ['angles'],\n },\n },\n)\n\nif (!frame || !frame.angles?.length) {\n return { ok: false, reason: 'framing failed', question, depth }\n}\n\nconst angles = frame.angles.slice(0, maxAngles)\n\nphase('Sweep')\nconst angleReports = await parallel(\n angles.map((a) => () =>\n agent(\n 'Investigate this angle. Use search and source-read tools liberally. Distinguish what you can confirm from what is conjecture.\\n\\n' +\n 'ANGLE: ' + a.title + '\\n' +\n 'KEY QUESTION: ' + a.key_question + '\\n\\n' +\n 'Return: 3\u20136 grounded findings (each with a 1-line claim and a source URL or file path), plus the strongest counter-evidence.',\n {\n label: a.title,\n toolset: RESEARCH_TOOLS,\n maxIterations: depth === 'deep' ? 40 : 30,\n schema: {\n type: 'object',\n properties: {\n findings: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n claim: { type: 'string' },\n source: { type: 'string' },\n confidence: { type: 'string', enum: ['low', 'med', 'high'] },\n },\n required: ['claim', 'source', 'confidence'],\n },\n },\n counterEvidence: { type: 'string' },\n },\n required: ['findings'],\n },\n },\n ),\n ),\n)\n\nphase('Synthesize')\nconst live = angleReports.filter(Boolean)\nconst synthesis = await agent(\n 'Synthesize a cited research report from these angle-level findings. Drop unsupported or duplicate claims. Use the highest-confidence source per claim. ' +\n 'Explicitly list contradictions where angles disagree. Return: an executive summary (max 5 sentences), top findings with inline source URLs, open questions, and contradictions.\\n\\n' +\n 'QUESTION:\\n' + question + '\\n\\n' +\n JSON.stringify({ angles, reports: live }, null, 2),\n {\n label: 'synthesis',\n schema: {\n type: 'object',\n properties: {\n executiveSummary: { type: 'string' },\n topFindings: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n claim: { type: 'string' },\n source: { type: 'string' },\n },\n required: ['claim', 'source'],\n },\n },\n openQuestions: { type: 'array', items: { type: 'string' } },\n contradictions: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n topic: { type: 'string' },\n sides: { type: 'array', items: { type: 'string' } },\n },\n required: ['topic', 'sides'],\n },\n },\n },\n required: ['executiveSummary', 'topFindings'],\n },\n },\n)\n\nreturn {\n ok: true,\n question,\n depth,\n angleCount: angles.length,\n ...(synthesis ?? { executiveSummary: 'synthesis failed', topFindings: [], contradictions: [] }),\n}\n";
@@ -6,11 +6,17 @@
6
6
  * exploration / source-reading angles in parallel, then synthesises a cited
7
7
  * report. Each angle is its own subagent so source reading does not pollute the
8
8
  * parent context.
9
+ *
10
+ * Args:
11
+ * - question: research question
12
+ * - depth: 'quick' (2 angles) | 'standard' (4) | 'deep' (6)
9
13
  */
10
14
  const RESEARCH_SCRIPT = `export const meta = {
11
15
  name: 'research',
12
16
  description: 'Multi-angle research on a question with parallel exploration and a cited synthesis.',
13
17
  whenToUse: 'User asks a non-trivial research question that benefits from multiple search angles or source reads.',
18
+ tags: ['research', 'investigation'],
19
+ estimatedAgents: { min: 4, max: 8 },
14
20
  phases: [
15
21
  { title: 'Frame' },
16
22
  { title: 'Sweep' },
@@ -18,13 +24,21 @@ const RESEARCH_SCRIPT = `export const meta = {
18
24
  ],
19
25
  }
20
26
 
27
+ const RESEARCH_TOOLS = ['web_search', 'web_fetch', 'read_file', 'grep', 'find', 'list_dir']
28
+
21
29
  const question = args && typeof args === 'object' && args.question
22
30
  ? String(args.question)
23
31
  : 'No explicit question supplied; infer from the most recent user turn.'
24
32
 
33
+ const depth = args && typeof args === 'object' && args.depth
34
+ ? String(args.depth)
35
+ : 'standard'
36
+ const maxAngles = depth === 'quick' ? 2 : depth === 'deep' ? 6 : 4
37
+
25
38
  phase('Frame')
26
39
  const frame = await agent(
27
- 'Frame this research question. Return 3–5 distinct angles worth investigating, plus the single most decisive sub-question for each. Be concrete.\\n\\nQUESTION:\\n' +
40
+ 'Frame this research question. Return exactly ' + maxAngles + ' distinct angles worth investigating, ' +
41
+ 'each with the single most decisive sub-question. Be concrete.\\n\\nQUESTION:\\n' +
28
42
  question,
29
43
  {
30
44
  label: 'framing',
@@ -49,12 +63,14 @@ const frame = await agent(
49
63
  )
50
64
 
51
65
  if (!frame || !frame.angles?.length) {
52
- return { ok: false, reason: 'framing failed', question }
66
+ return { ok: false, reason: 'framing failed', question, depth }
53
67
  }
54
68
 
69
+ const angles = frame.angles.slice(0, maxAngles)
70
+
55
71
  phase('Sweep')
56
72
  const angleReports = await parallel(
57
- frame.angles.map((a) => () =>
73
+ angles.map((a) => () =>
58
74
  agent(
59
75
  'Investigate this angle. Use search and source-read tools liberally. Distinguish what you can confirm from what is conjecture.\\n\\n' +
60
76
  'ANGLE: ' + a.title + '\\n' +
@@ -62,6 +78,8 @@ const angleReports = await parallel(
62
78
  'Return: 3–6 grounded findings (each with a 1-line claim and a source URL or file path), plus the strongest counter-evidence.',
63
79
  {
64
80
  label: a.title,
81
+ toolset: RESEARCH_TOOLS,
82
+ maxIterations: depth === 'deep' ? 40 : 30,
65
83
  schema: {
66
84
  type: 'object',
67
85
  properties: {
@@ -90,9 +108,9 @@ phase('Synthesize')
90
108
  const live = angleReports.filter(Boolean)
91
109
  const synthesis = await agent(
92
110
  'Synthesize a cited research report from these angle-level findings. Drop unsupported or duplicate claims. Use the highest-confidence source per claim. ' +
93
- 'Return: an executive summary (max 5 sentences), a bullet list of top findings with inline source URLs, and one section listing open questions.\\n\\n' +
111
+ 'Explicitly list contradictions where angles disagree. Return: an executive summary (max 5 sentences), top findings with inline source URLs, open questions, and contradictions.\\n\\n' +
94
112
  'QUESTION:\\n' + question + '\\n\\n' +
95
- JSON.stringify({ angles: frame.angles, reports: live }, null, 2),
113
+ JSON.stringify({ angles, reports: live }, null, 2),
96
114
  {
97
115
  label: 'synthesis',
98
116
  schema: {
@@ -111,6 +129,17 @@ const synthesis = await agent(
111
129
  },
112
130
  },
113
131
  openQuestions: { type: 'array', items: { type: 'string' } },
132
+ contradictions: {
133
+ type: 'array',
134
+ items: {
135
+ type: 'object',
136
+ properties: {
137
+ topic: { type: 'string' },
138
+ sides: { type: 'array', items: { type: 'string' } },
139
+ },
140
+ required: ['topic', 'sides'],
141
+ },
142
+ },
114
143
  },
115
144
  required: ['executiveSummary', 'topFindings'],
116
145
  },
@@ -120,7 +149,9 @@ const synthesis = await agent(
120
149
  return {
121
150
  ok: true,
122
151
  question,
123
- ...(synthesis ?? { executiveSummary: 'synthesis failed', topFindings: [] }),
152
+ depth,
153
+ angleCount: angles.length,
154
+ ...(synthesis ?? { executiveSummary: 'synthesis failed', topFindings: [], contradictions: [] }),
124
155
  }
125
156
  `;
126
157
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"research.js","names":[],"sources":["../../../../../src/agent/workflow/builtins/research.ts"],"sourcesContent":["/**\n * Built-in workflow: `research`\n *\n * Multi-modal research sweep on a question (args.question). Fans out search /\n * exploration / source-reading angles in parallel, then synthesises a cited\n * report. Each angle is its own subagent so source reading does not pollute the\n * parent context.\n */\n\nexport const RESEARCH_SCRIPT = `export const meta = {\n name: 'research',\n description: 'Multi-angle research on a question with parallel exploration and a cited synthesis.',\n whenToUse: 'User asks a non-trivial research question that benefits from multiple search angles or source reads.',\n phases: [\n { title: 'Frame' },\n { title: 'Sweep' },\n { title: 'Synthesize' },\n ],\n}\n\nconst question = args && typeof args === 'object' && args.question\n ? String(args.question)\n : 'No explicit question supplied; infer from the most recent user turn.'\n\nphase('Frame')\nconst frame = await agent(\n 'Frame this research question. Return 3–5 distinct angles worth investigating, plus the single most decisive sub-question for each. Be concrete.\\\\n\\\\nQUESTION:\\\\n' +\n question,\n {\n label: 'framing',\n schema: {\n type: 'object',\n properties: {\n angles: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n key_question: { type: 'string' },\n },\n required: ['title', 'key_question'],\n },\n },\n },\n required: ['angles'],\n },\n },\n)\n\nif (!frame || !frame.angles?.length) {\n return { ok: false, reason: 'framing failed', question }\n}\n\nphase('Sweep')\nconst angleReports = await parallel(\n frame.angles.map((a) => () =>\n agent(\n 'Investigate this angle. Use search and source-read tools liberally. Distinguish what you can confirm from what is conjecture.\\\\n\\\\n' +\n 'ANGLE: ' + a.title + '\\\\n' +\n 'KEY QUESTION: ' + a.key_question + '\\\\n\\\\n' +\n 'Return: 3–6 grounded findings (each with a 1-line claim and a source URL or file path), plus the strongest counter-evidence.',\n {\n label: a.title,\n schema: {\n type: 'object',\n properties: {\n findings: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n claim: { type: 'string' },\n source: { type: 'string' },\n confidence: { type: 'string', enum: ['low', 'med', 'high'] },\n },\n required: ['claim', 'source', 'confidence'],\n },\n },\n counterEvidence: { type: 'string' },\n },\n required: ['findings'],\n },\n },\n ),\n ),\n)\n\nphase('Synthesize')\nconst live = angleReports.filter(Boolean)\nconst synthesis = await agent(\n 'Synthesize a cited research report from these angle-level findings. Drop unsupported or duplicate claims. Use the highest-confidence source per claim. ' +\n 'Return: an executive summary (max 5 sentences), a bullet list of top findings with inline source URLs, and one section listing open questions.\\\\n\\\\n' +\n 'QUESTION:\\\\n' + question + '\\\\n\\\\n' +\n JSON.stringify({ angles: frame.angles, reports: live }, null, 2),\n {\n label: 'synthesis',\n schema: {\n type: 'object',\n properties: {\n executiveSummary: { type: 'string' },\n topFindings: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n claim: { type: 'string' },\n source: { type: 'string' },\n },\n required: ['claim', 'source'],\n },\n },\n openQuestions: { type: 'array', items: { type: 'string' } },\n },\n required: ['executiveSummary', 'topFindings'],\n },\n },\n)\n\nreturn {\n ok: true,\n question,\n ...(synthesis ?? { executiveSummary: 'synthesis failed', topFindings: [] }),\n}\n`\n"],"mappings":";;;;;;;;;AASA,MAAa,kBAAkB"}
1
+ {"version":3,"file":"research.js","names":[],"sources":["../../../../../src/agent/workflow/builtins/research.ts"],"sourcesContent":["/**\n * Built-in workflow: `research`\n *\n * Multi-modal research sweep on a question (args.question). Fans out search /\n * exploration / source-reading angles in parallel, then synthesises a cited\n * report. Each angle is its own subagent so source reading does not pollute the\n * parent context.\n *\n * Args:\n * - question: research question\n * - depth: 'quick' (2 angles) | 'standard' (4) | 'deep' (6)\n */\n\nexport const RESEARCH_SCRIPT = `export const meta = {\n name: 'research',\n description: 'Multi-angle research on a question with parallel exploration and a cited synthesis.',\n whenToUse: 'User asks a non-trivial research question that benefits from multiple search angles or source reads.',\n tags: ['research', 'investigation'],\n estimatedAgents: { min: 4, max: 8 },\n phases: [\n { title: 'Frame' },\n { title: 'Sweep' },\n { title: 'Synthesize' },\n ],\n}\n\nconst RESEARCH_TOOLS = ['web_search', 'web_fetch', 'read_file', 'grep', 'find', 'list_dir']\n\nconst question = args && typeof args === 'object' && args.question\n ? String(args.question)\n : 'No explicit question supplied; infer from the most recent user turn.'\n\nconst depth = args && typeof args === 'object' && args.depth\n ? String(args.depth)\n : 'standard'\nconst maxAngles = depth === 'quick' ? 2 : depth === 'deep' ? 6 : 4\n\nphase('Frame')\nconst frame = await agent(\n 'Frame this research question. Return exactly ' + maxAngles + ' distinct angles worth investigating, ' +\n 'each with the single most decisive sub-question. Be concrete.\\\\n\\\\nQUESTION:\\\\n' +\n question,\n {\n label: 'framing',\n schema: {\n type: 'object',\n properties: {\n angles: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n key_question: { type: 'string' },\n },\n required: ['title', 'key_question'],\n },\n },\n },\n required: ['angles'],\n },\n },\n)\n\nif (!frame || !frame.angles?.length) {\n return { ok: false, reason: 'framing failed', question, depth }\n}\n\nconst angles = frame.angles.slice(0, maxAngles)\n\nphase('Sweep')\nconst angleReports = await parallel(\n angles.map((a) => () =>\n agent(\n 'Investigate this angle. Use search and source-read tools liberally. Distinguish what you can confirm from what is conjecture.\\\\n\\\\n' +\n 'ANGLE: ' + a.title + '\\\\n' +\n 'KEY QUESTION: ' + a.key_question + '\\\\n\\\\n' +\n 'Return: 3–6 grounded findings (each with a 1-line claim and a source URL or file path), plus the strongest counter-evidence.',\n {\n label: a.title,\n toolset: RESEARCH_TOOLS,\n maxIterations: depth === 'deep' ? 40 : 30,\n schema: {\n type: 'object',\n properties: {\n findings: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n claim: { type: 'string' },\n source: { type: 'string' },\n confidence: { type: 'string', enum: ['low', 'med', 'high'] },\n },\n required: ['claim', 'source', 'confidence'],\n },\n },\n counterEvidence: { type: 'string' },\n },\n required: ['findings'],\n },\n },\n ),\n ),\n)\n\nphase('Synthesize')\nconst live = angleReports.filter(Boolean)\nconst synthesis = await agent(\n 'Synthesize a cited research report from these angle-level findings. Drop unsupported or duplicate claims. Use the highest-confidence source per claim. ' +\n 'Explicitly list contradictions where angles disagree. Return: an executive summary (max 5 sentences), top findings with inline source URLs, open questions, and contradictions.\\\\n\\\\n' +\n 'QUESTION:\\\\n' + question + '\\\\n\\\\n' +\n JSON.stringify({ angles, reports: live }, null, 2),\n {\n label: 'synthesis',\n schema: {\n type: 'object',\n properties: {\n executiveSummary: { type: 'string' },\n topFindings: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n claim: { type: 'string' },\n source: { type: 'string' },\n },\n required: ['claim', 'source'],\n },\n },\n openQuestions: { type: 'array', items: { type: 'string' } },\n contradictions: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n topic: { type: 'string' },\n sides: { type: 'array', items: { type: 'string' } },\n },\n required: ['topic', 'sides'],\n },\n },\n },\n required: ['executiveSummary', 'topFindings'],\n },\n },\n)\n\nreturn {\n ok: true,\n question,\n depth,\n angleCount: angles.length,\n ...(synthesis ?? { executiveSummary: 'synthesis failed', topFindings: [], contradictions: [] }),\n}\n`\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,kBAAkB"}
@@ -24,6 +24,11 @@ export interface CatalogEntry {
24
24
  path: string | null;
25
25
  description: string;
26
26
  whenToUse?: string;
27
+ tags?: string[];
28
+ estimatedAgents?: {
29
+ min: number;
30
+ max: number;
31
+ };
27
32
  }
28
33
  export interface LoadedWorkflow {
29
34
  name: string;
@@ -34,7 +34,9 @@ function createWorkflowCatalog(opts = {}) {
34
34
  source: "builtin",
35
35
  path: null,
36
36
  description: meta?.description ?? "(unparseable)",
37
- whenToUse: meta?.whenToUse
37
+ whenToUse: meta?.whenToUse,
38
+ tags: meta?.tags,
39
+ estimatedAgents: meta?.estimatedAgents
38
40
  });
39
41
  }
40
42
  for (const file of safeListUserFiles(userDir)) {
@@ -47,7 +49,9 @@ function createWorkflowCatalog(opts = {}) {
47
49
  source: "user",
48
50
  path: full,
49
51
  description: meta?.description ?? "(unparseable)",
50
- whenToUse: meta?.whenToUse
52
+ whenToUse: meta?.whenToUse,
53
+ tags: meta?.tags,
54
+ estimatedAgents: meta?.estimatedAgents
51
55
  });
52
56
  }
53
57
  return [...entries.values()].sort((a, b) => a.name.localeCompare(b.name));
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.js","names":[],"sources":["../../../../src/agent/workflow/catalog.ts"],"sourcesContent":["/**\n * Catalog for named workflows.\n *\n * Resolution order (built-ins are starting points, user workflows win):\n * 1. `~/.xopc/workflows/<name>.js` (or `<name>.workflow.js`)\n * 2. {@link BUILTIN_WORKFLOWS}\n *\n * The user dir is discovered via {@link resolveStateDir}, so `XOPC_STATE_DIR`\n * overrides apply automatically (matches how skills / extensions are wired).\n *\n * Listing is filesystem-cheap (single `readdir`) and runs synchronously — the\n * `/workflows` slash command is interactive and should return immediately.\n *\n * Validation: on load we re-parse the script to make sure `meta.name` matches\n * the filename. This prevents copy-pasted scripts from being silently\n * mis-addressed when invoked by name.\n */\n\nimport {\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs';\nimport { join } from 'node:path';\n\nimport { resolveStateDir } from '../../config/paths-state.js';\n\nimport { BUILTIN_WORKFLOWS } from './builtins/index.js';\nimport { parseWorkflowScript } from './parser.js';\nimport type { WorkflowMeta } from './types.js';\n\nexport type WorkflowSource = 'user' | 'builtin';\n\nexport interface CatalogEntry {\n name: string;\n source: WorkflowSource;\n /** Absolute path for user entries; null for built-ins (in-memory). */\n path: string | null;\n description: string;\n whenToUse?: string;\n}\n\nexport interface LoadedWorkflow {\n name: string;\n source: WorkflowSource;\n script: string;\n meta: WorkflowMeta;\n path: string | null;\n}\n\nexport interface WorkflowCatalog {\n list(): CatalogEntry[];\n /** Load a named workflow. Throws if missing or meta.name disagrees with filename. */\n load(name: string): LoadedWorkflow;\n /** Save a script as a user workflow. Throws if the script fails to parse. */\n save(name: string, script: string): { path: string };\n /** Remove a user workflow. No-op if absent. Built-ins are never removed. */\n remove(name: string): boolean;\n /** Absolute path to the user workflows directory (created lazily on save). */\n userDir: string;\n}\n\nconst NAME_RE = /^[a-z][a-z0-9_-]*$/;\n\nexport function createWorkflowCatalog(opts: { userDir?: string } = {}): WorkflowCatalog {\n const userDir = opts.userDir ?? defaultUserDir();\n\n const list = (): CatalogEntry[] => {\n const entries = new Map<string, CatalogEntry>();\n for (const b of BUILTIN_WORKFLOWS) {\n const meta = safeMeta(b.script);\n entries.set(b.name, {\n name: b.name,\n source: 'builtin',\n path: null,\n description: meta?.description ?? '(unparseable)',\n whenToUse: meta?.whenToUse,\n });\n }\n for (const file of safeListUserFiles(userDir)) {\n const name = stripExt(file);\n if (!isValidName(name)) continue;\n const full = join(userDir, file);\n const meta = safeMeta(readScript(full));\n // User wins on collision.\n entries.set(name, {\n name,\n source: 'user',\n path: full,\n description: meta?.description ?? '(unparseable)',\n whenToUse: meta?.whenToUse,\n });\n }\n return [...entries.values()].sort((a, b) => a.name.localeCompare(b.name));\n };\n\n const load = (name: string): LoadedWorkflow => {\n requireValidName(name);\n const userPath = findUserPath(userDir, name);\n if (userPath) {\n const script = readScript(userPath);\n const { meta } = parseWorkflowScript(script);\n ensureMetaNameMatches(meta, name, userPath);\n return { name, source: 'user', script, meta, path: userPath };\n }\n const builtin = BUILTIN_WORKFLOWS.find((b) => b.name === name);\n if (builtin) {\n const { meta } = parseWorkflowScript(builtin.script);\n ensureMetaNameMatches(meta, name, '<builtin>');\n return { name, source: 'builtin', script: builtin.script, meta, path: null };\n }\n throw new Error(\n `workflow not found: ${name}. Drop a script at ${join(userDir, `${name}.js`)} or pick one of: ${list()\n .map((e) => e.name)\n .join(', ')}`,\n );\n };\n\n const save = (name: string, script: string): { path: string } => {\n requireValidName(name);\n const { meta } = parseWorkflowScript(script);\n if (meta.name !== name) {\n throw new Error(\n `meta.name \"${meta.name}\" does not match save name \"${name}\". Adjust one to match the other.`,\n );\n }\n if (!existsSync(userDir)) {\n mkdirSync(userDir, { recursive: true });\n }\n const path = join(userDir, `${name}.js`);\n writeFileSync(path, normalizeNewlines(script), 'utf-8');\n return { path };\n };\n\n const remove = (name: string): boolean => {\n requireValidName(name);\n const userPath = findUserPath(userDir, name);\n if (!userPath) return false;\n unlinkSync(userPath);\n return true;\n };\n\n return { list, load, save, remove, userDir };\n}\n\n// ---------------------------------------------------------------------------\n\nexport function defaultUserDir(): string {\n return join(resolveStateDir(), 'workflows');\n}\n\nfunction safeListUserFiles(dir: string): string[] {\n try {\n if (!existsSync(dir)) return [];\n const st = statSync(dir);\n if (!st.isDirectory()) return [];\n return readdirSync(dir).filter((f) => /\\.(js|workflow\\.js)$/i.test(f));\n } catch {\n return [];\n }\n}\n\nfunction findUserPath(dir: string, name: string): string | null {\n for (const candidate of [`${name}.js`, `${name}.workflow.js`]) {\n const full = join(dir, candidate);\n if (existsSync(full)) return full;\n }\n return null;\n}\n\nfunction readScript(path: string): string {\n return readFileSync(path, 'utf-8');\n}\n\nfunction safeMeta(script: string): WorkflowMeta | null {\n try {\n return parseWorkflowScript(script).meta;\n } catch {\n return null;\n }\n}\n\nfunction stripExt(filename: string): string {\n return filename.replace(/\\.workflow\\.js$/i, '').replace(/\\.js$/i, '');\n}\n\nfunction isValidName(name: string): boolean {\n return NAME_RE.test(name);\n}\n\nfunction requireValidName(name: string): void {\n if (!isValidName(name)) {\n throw new Error(`invalid workflow name \"${name}\". Use lowercase snake_case, e.g. \"audit_repo\".`);\n }\n}\n\nfunction ensureMetaNameMatches(meta: WorkflowMeta, name: string, locator: string): void {\n if (meta.name !== name) {\n throw new Error(\n `workflow ${locator}: meta.name \"${meta.name}\" disagrees with addressable name \"${name}\". ` +\n 'Rename the file or the meta.name to match.',\n );\n }\n}\n\nfunction normalizeNewlines(s: string): string {\n return s.endsWith('\\n') ? s : `${s}\\n`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"catalog.js","names":[],"sources":["../../../../src/agent/workflow/catalog.ts"],"sourcesContent":["/**\n * Catalog for named workflows.\n *\n * Resolution order (built-ins are starting points, user workflows win):\n * 1. `~/.xopc/workflows/<name>.js` (or `<name>.workflow.js`)\n * 2. {@link BUILTIN_WORKFLOWS}\n *\n * The user dir is discovered via {@link resolveStateDir}, so `XOPC_STATE_DIR`\n * overrides apply automatically (matches how skills / extensions are wired).\n *\n * Listing is filesystem-cheap (single `readdir`) and runs synchronously — the\n * `/workflows` slash command is interactive and should return immediately.\n *\n * Validation: on load we re-parse the script to make sure `meta.name` matches\n * the filename. This prevents copy-pasted scripts from being silently\n * mis-addressed when invoked by name.\n */\n\nimport {\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs';\nimport { join } from 'node:path';\n\nimport { resolveStateDir } from '../../config/paths-state.js';\n\nimport { BUILTIN_WORKFLOWS } from './builtins/index.js';\nimport { parseWorkflowScript } from './parser.js';\nimport type { WorkflowMeta } from './types.js';\n\nexport type WorkflowSource = 'user' | 'builtin';\n\nexport interface CatalogEntry {\n name: string;\n source: WorkflowSource;\n /** Absolute path for user entries; null for built-ins (in-memory). */\n path: string | null;\n description: string;\n whenToUse?: string;\n tags?: string[];\n estimatedAgents?: { min: number; max: number };\n}\n\nexport interface LoadedWorkflow {\n name: string;\n source: WorkflowSource;\n script: string;\n meta: WorkflowMeta;\n path: string | null;\n}\n\nexport interface WorkflowCatalog {\n list(): CatalogEntry[];\n /** Load a named workflow. Throws if missing or meta.name disagrees with filename. */\n load(name: string): LoadedWorkflow;\n /** Save a script as a user workflow. Throws if the script fails to parse. */\n save(name: string, script: string): { path: string };\n /** Remove a user workflow. No-op if absent. Built-ins are never removed. */\n remove(name: string): boolean;\n /** Absolute path to the user workflows directory (created lazily on save). */\n userDir: string;\n}\n\nconst NAME_RE = /^[a-z][a-z0-9_-]*$/;\n\nexport function createWorkflowCatalog(opts: { userDir?: string } = {}): WorkflowCatalog {\n const userDir = opts.userDir ?? defaultUserDir();\n\n const list = (): CatalogEntry[] => {\n const entries = new Map<string, CatalogEntry>();\n for (const b of BUILTIN_WORKFLOWS) {\n const meta = safeMeta(b.script);\n entries.set(b.name, {\n name: b.name,\n source: 'builtin',\n path: null,\n description: meta?.description ?? '(unparseable)',\n whenToUse: meta?.whenToUse,\n tags: meta?.tags,\n estimatedAgents: meta?.estimatedAgents,\n });\n }\n for (const file of safeListUserFiles(userDir)) {\n const name = stripExt(file);\n if (!isValidName(name)) continue;\n const full = join(userDir, file);\n const meta = safeMeta(readScript(full));\n // User wins on collision.\n entries.set(name, {\n name,\n source: 'user',\n path: full,\n description: meta?.description ?? '(unparseable)',\n whenToUse: meta?.whenToUse,\n tags: meta?.tags,\n estimatedAgents: meta?.estimatedAgents,\n });\n }\n return [...entries.values()].sort((a, b) => a.name.localeCompare(b.name));\n };\n\n const load = (name: string): LoadedWorkflow => {\n requireValidName(name);\n const userPath = findUserPath(userDir, name);\n if (userPath) {\n const script = readScript(userPath);\n const { meta } = parseWorkflowScript(script);\n ensureMetaNameMatches(meta, name, userPath);\n return { name, source: 'user', script, meta, path: userPath };\n }\n const builtin = BUILTIN_WORKFLOWS.find((b) => b.name === name);\n if (builtin) {\n const { meta } = parseWorkflowScript(builtin.script);\n ensureMetaNameMatches(meta, name, '<builtin>');\n return { name, source: 'builtin', script: builtin.script, meta, path: null };\n }\n throw new Error(\n `workflow not found: ${name}. Drop a script at ${join(userDir, `${name}.js`)} or pick one of: ${list()\n .map((e) => e.name)\n .join(', ')}`,\n );\n };\n\n const save = (name: string, script: string): { path: string } => {\n requireValidName(name);\n const { meta } = parseWorkflowScript(script);\n if (meta.name !== name) {\n throw new Error(\n `meta.name \"${meta.name}\" does not match save name \"${name}\". Adjust one to match the other.`,\n );\n }\n if (!existsSync(userDir)) {\n mkdirSync(userDir, { recursive: true });\n }\n const path = join(userDir, `${name}.js`);\n writeFileSync(path, normalizeNewlines(script), 'utf-8');\n return { path };\n };\n\n const remove = (name: string): boolean => {\n requireValidName(name);\n const userPath = findUserPath(userDir, name);\n if (!userPath) return false;\n unlinkSync(userPath);\n return true;\n };\n\n return { list, load, save, remove, userDir };\n}\n\n// ---------------------------------------------------------------------------\n\nexport function defaultUserDir(): string {\n return join(resolveStateDir(), 'workflows');\n}\n\nfunction safeListUserFiles(dir: string): string[] {\n try {\n if (!existsSync(dir)) return [];\n const st = statSync(dir);\n if (!st.isDirectory()) return [];\n return readdirSync(dir).filter((f) => /\\.(js|workflow\\.js)$/i.test(f));\n } catch {\n return [];\n }\n}\n\nfunction findUserPath(dir: string, name: string): string | null {\n for (const candidate of [`${name}.js`, `${name}.workflow.js`]) {\n const full = join(dir, candidate);\n if (existsSync(full)) return full;\n }\n return null;\n}\n\nfunction readScript(path: string): string {\n return readFileSync(path, 'utf-8');\n}\n\nfunction safeMeta(script: string): WorkflowMeta | null {\n try {\n return parseWorkflowScript(script).meta;\n } catch {\n return null;\n }\n}\n\nfunction stripExt(filename: string): string {\n return filename.replace(/\\.workflow\\.js$/i, '').replace(/\\.js$/i, '');\n}\n\nfunction isValidName(name: string): boolean {\n return NAME_RE.test(name);\n}\n\nfunction requireValidName(name: string): void {\n if (!isValidName(name)) {\n throw new Error(`invalid workflow name \"${name}\". Use lowercase snake_case, e.g. \"audit_repo\".`);\n }\n}\n\nfunction ensureMetaNameMatches(meta: WorkflowMeta, name: string, locator: string): void {\n if (meta.name !== name) {\n throw new Error(\n `workflow ${locator}: meta.name \"${meta.name}\" disagrees with addressable name \"${name}\". ` +\n 'Rename the file or the meta.name to match.',\n );\n }\n}\n\nfunction normalizeNewlines(s: string): string {\n return s.endsWith('\\n') ? s : `${s}\\n`;\n}\n"],"mappings":"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"}