hive-lite 0.2.0 → 0.2.1

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.
@@ -193,9 +193,9 @@ Saved handoffs are written under:
193
193
  .hive/state/intents/<intent_id>/start-intent.json
194
194
  ```
195
195
 
196
- The validator requires `type: hive-lite.start-intent`, `version: 1`, `status: ready_for_hive`, a safe `intent_id`, required Markdown sections, and `Blocking Questions` set to `None`.
196
+ The validator requires `type: hive-lite.start-intent`, `version: 1`, `status: ready_for_hive`, a safe `intent_id`, and the minimal Markdown sections `Original Idea`, `Agreed Direction`, and `User-Confirmed Decisions`. `Blocking Questions` is optional, but if present for a ready handoff it must be `None`.
197
197
  It rejects execution-only content such as `Direct Writable`, final file scope, implementation plans, validation plans, files-to-modify sections, and direct instructions that an agent should edit specific files.
198
- `show --json` includes a derived `findIntent` for `$hive-lite-start-prompt`.
198
+ `show --json` includes a derived `findIntent` for `$hive-lite-start-prompt`; it is target-oriented and derived from `Agreed Direction`.
199
199
  The human-readable `intent save` output recommends starting the operator runner with `hive-lite operator run --intent <intent_id>` and also prints the equivalent saved handoff path form.
200
200
 
201
201
  ## `skills`
@@ -1,16 +1,18 @@
1
1
  ---
2
2
  name: hive-lite-discovery
3
- description: Use this skill when a user has a vague or exploratory product idea that is not ready for Hive Lite start/find yet. It runs a pre-Hive, pre-operator clarification flow, may inspect the repository read-only, asks only blocking product questions, drafts and saves a Start Intent Handoff with `hive-lite intent save`, and stops before code execution, Project Map repair, or operator-run automation.
3
+ description: Use this skill when a user wants to discuss and clarify a product/code requirement before coding. It behaves like a no-code requirements conversation, may inspect the repository read-only to compare the idea with the current project, asks follow-up questions when the requirement is unclear, saves a short Start Intent Handoff with `hive-lite intent save`, and stops before code execution, Project Map repair, or operator-run automation.
4
4
  ---
5
5
 
6
6
  # Hive Lite Discovery
7
7
 
8
8
  ## Purpose
9
9
 
10
- Turn a vague idea into a durable Start Intent Handoff that is ready for Hive Lite to search.
10
+ Turn a requirements conversation into a durable Start Intent Handoff that is ready for Hive Lite to search.
11
11
 
12
12
  Discovery makes an idea ready for Hive Lite. Start makes that intent ready for code.
13
13
 
14
+ The mental model is simple: the user is saying, "do not write code yet; discuss this requirement with me." If the requirement is unclear, ask natural follow-up questions. If repo context would help, inspect it read-only. When the direction is clear enough, save the agreed result for the next skill.
15
+
14
16
  ## Boundaries
15
17
 
16
18
  - This skill is pre-Hive and pre-operator.
@@ -18,16 +20,15 @@ Discovery makes an idea ready for Hive Lite. Start makes that intent ready for c
18
20
  - Do not run `hive-lite find`, `check`, `validate`, `accept`, `operator run`, or coding-agent execution.
19
21
  - Do not produce `Direct Writable`, final file scope, an implementation plan, a validation plan, files to modify, or instructions that an agent should edit specific files.
20
22
  - Do not turn vague ideas into coding permission. The handoff is search-ready, not execution-ready.
21
- - Product decisions belong in `User-Confirmed Decisions` or `Blocking Questions`, not `Unknowns For Hive Lite To Resolve`.
22
- - `Unknowns For Hive Lite To Resolve` must contain codebase/context unknowns that Hive Lite can answer from Project Map and Context Packet discovery.
23
+ - Keep the handoff plain and short. Do not add sections merely to sound complete.
24
+ - Optional non-goals, acceptance signals, unknowns, or hints are for unusual cases only; omit them by default.
23
25
 
24
26
  ## Completion Contract
25
27
 
26
28
  End with exactly one clear outcome:
27
29
 
28
30
  - `ready_for_hive`: a valid Start Intent Handoff is saved, `intent_id` and artifact paths are reported, and the next terminal `hive-lite operator run --intent ...` command is shown.
29
- - `needs_user_decision`: one to three blocking product questions are asked; no `ready_for_hive` handoff is saved.
30
- - `skip_discovery`: the requirement is already clear; provide the exact `$hive-lite-start-prompt` handoff and stop.
31
+ - `needs_user_decision`: one to three product questions are asked because the direction is not clear enough; no `ready_for_hive` handoff is saved.
31
32
  - `blocked`: CLI, repo, or artifact validation prevents saving; report the exact blocker and next action.
32
33
 
33
34
  Do not produce a coding prompt, implementation plan, or file-edit permission as a discovery outcome.
@@ -38,20 +39,21 @@ Gather only enough facts to make the idea ready for Hive Lite search.
38
39
 
39
40
  Stop searching once these are known:
40
41
 
41
- - the user-approved product direction and non-goals;
42
+ - the user-approved product direction;
42
43
  - confirmed product decisions versus open blocking questions;
43
- - only the minimal repo vocabulary or existing capability hints needed to phrase the handoff;
44
+ - whether the request is best understood as a bug, new capability, behavior change, or replacement of existing behavior;
45
+ - only the minimal repo vocabulary or existing capability hints needed to phrase the direction in project terms;
44
46
  - whether the handoff can be saved as `status: ready_for_hive`.
45
47
 
46
48
  Do not inspect the repo to choose final files, architecture, validation commands, or Direct Writable scope. Those belong to `$hive-lite-start-prompt` and the Context Packet.
47
49
 
48
50
  ## Workflow
49
51
 
50
- 1. Understand the original vague idea and preserve it as provenance.
51
- 2. Inspect the repo read-only only when project vocabulary or feasibility needs clarification. Prefer `rg` and `rg --files`; do not write files during exploration.
52
- 3. Ask only blocking product questions. If product decisions remain unresolved, do not save a `ready_for_hive` handoff.
52
+ 1. Understand the user's starting idea and preserve it as provenance.
53
+ 2. Inspect the repo read-only only when project vocabulary, current behavior, or the difference between the idea and the current project needs clarification. Prefer `rg` and `rg --files`; do not write files during exploration.
54
+ 3. Ask follow-up questions only when the answer affects the product direction. If product decisions remain unresolved, do not save a `ready_for_hive` handoff.
53
55
  4. Separate confirmed decisions from ideas that were merely discussed.
54
- 5. Draft a Start Intent Handoff using the required structure below.
56
+ 5. Draft a short Start Intent Handoff using the required structure below.
55
57
  6. Save it with one of:
56
58
 
57
59
  ```bash
@@ -59,7 +61,8 @@ hive-lite intent save --from <draft-start-intent.md> --json
59
61
  hive-lite intent save --stdin --json
60
62
  ```
61
63
 
62
- 7. If the CLI reports errors, revise the draft and save again. Treat execution-only content errors as real scope violations, not formatting nits.
64
+ 7. If the CLI reports errors, revise the draft only when the error is about this draft's content. Treat execution-only content errors as real scope violations, not formatting nits.
65
+ - If the CLI reports `MISSING_SECTION` for optional legacy sections such as `Non-Goals`, `Acceptance Signals`, `Unknowns For Hive Lite To Resolve`, `Pre-Hive Observations / Hints`, or `Blocking Questions`, the installed CLI is stale relative to this skill. Do not add those sections just to satisfy the old CLI. Stop and tell the user to update or reinstall Hive Lite CLI, then rerun discovery.
63
66
  8. Return the `intent_id`, `start-intent.md` path, and the next terminal command for `hive-lite operator run --intent`.
64
67
 
65
68
  ## Required Handoff
@@ -90,37 +93,32 @@ created_at: 2026-05-15T00:00:00+08:00
90
93
 
91
94
  - <Decision actually confirmed by the user.>
92
95
 
93
- ## Non-Goals
94
-
95
- - <Scope boundary.>
96
- - Do not commit to exact files, Direct Writable scope, or implementation architecture before Hive Lite context discovery.
97
-
98
- ## Acceptance Signals
99
-
100
- - <User-level signal that the direction would feel complete. This is not a validation plan.>
101
-
102
- ## Unknowns For Hive Lite To Resolve
103
-
104
- - <Codebase/context question, such as where an existing provider abstraction lives.>
96
+ ```
105
97
 
106
- ## Pre-Hive Observations / Hints
98
+ The sections above are enough for most handoffs. Add optional sections only when they remove real ambiguity:
107
99
 
108
- These are non-authoritative hints from discovery. Hive Lite must verify them through the Project Map and Context Packet.
100
+ - `## Blocking Questions`: use only when the handoff is not ready; for `ready_for_hive`, omit it or set it to `None`.
101
+ - `## Non-Goals`: use only for a user-confirmed scope boundary that would otherwise be easy to misunderstand.
102
+ - `## Pre-Hive Observations / Hints`: use only for non-authoritative repo observations that materially help wording.
103
+ - Avoid `## Acceptance Signals` and `## Unknowns For Hive Lite To Resolve` by default. They easily become noisy pseudo-specs.
109
104
 
110
- - <Optional read-only observation phrased as may/appears/observed/verify.>
105
+ ## Target-Only Routing Text
111
106
 
112
- ## Blocking Questions
107
+ `Agreed Direction` is used to derive the Hive Lite `findIntent`. Keep it target-only.
113
108
 
114
- None.
115
- ```
109
+ - Put only the capability or behavior that should be searched next.
110
+ - Do not name reference, existing, non-regression, or "do not replace" capabilities in `Agreed Direction`.
111
+ - If the user mentions an existing capability only as context, keep that in `Original Idea`.
112
+ - If an additive/non-replacement boundary is important, phrase it generically in `User-Confirmed Decisions`, such as `This is additive, not a replacement of existing LLM proxy capability`, without naming unrelated providers.
113
+ - Before saving, mentally check whether `Agreed Direction` could match multiple providers, products, modules, or target identities. If yes, rewrite it around the intended target only.
116
114
 
117
115
  ## Validation Rules
118
116
 
119
- - A `ready_for_hive` handoff must have `Blocking Questions` set to `None`.
120
- - Use `Acceptance Signals` for user-facing completion signals, not exact validation commands.
117
+ - A `ready_for_hive` handoff must have no unresolved product questions. If `Blocking Questions` exists, it must be `None`.
118
+ - Do not invent non-goals, acceptance criteria, codebase unknowns, or hints just to fill a template.
121
119
  - Put possible file paths only in `Pre-Hive Observations / Hints`, and phrase them as non-authoritative hints.
122
120
  - If the user asks for implementation planning, explain that `$hive-lite-start-prompt` and the Context Packet define execution boundaries after discovery.
123
- - If the user already has a clear coding requirement, skip discovery and hand off to `$hive-lite-start-prompt` with the requirement text.
121
+ - If the requirement is already clear, save a short handoff without extra questions. Do not skip to `$hive-lite-start-prompt` unless the user explicitly asks to bypass discovery.
124
122
 
125
123
  ## Final Output
126
124
 
@@ -42,7 +42,7 @@ If the invocation includes `Step result path`, `Artifacts directory`, and `Step-
42
42
  - Write copyable artifacts under the provided `Artifacts directory`, then write `step-result.json` to the exact `Step result path`.
43
43
  - For `prompt_ready`, write the coding prompt artifact and set `recommended_action` to `copy_coding_prompt`.
44
44
  - For `split_required`, write a ready-phase handoff prompt artifact and set `recommended_action` to `start_ready_phase`.
45
- - For map-maintainer handoffs, use `map_update_required` with `recommended_action: map_update_required`.
45
+ - For map-maintainer handoffs or map repairs that must be committed/stashed before continuing, write `Artifacts directory/map-update-required.md`, use `map_update_required` with `recommended_action: map_update_required`, and set `artifacts.map_update_required_path`.
46
46
  - For setup, preflight, validation, or irrecoverable start blockers, use `blocked` with `recommended_action: revise_required`, or `failed` for unexpected tool/runtime failures.
47
47
  - After writing the structured result, stop. Do not wait for human input inside the operator-agent session.
48
48
 
@@ -179,12 +179,13 @@ hive-lite intent validate <intent_id_or_path> --json
179
179
  hive-lite intent show <intent_id_or_path> --json
180
180
  ```
181
181
 
182
- Use the JSON output for decisions. Refuse and send the user back to `$hive-lite-discovery` if validation is not valid, `status` is not `ready_for_hive`, or `fields.blockingQuestions` is not `None`.
182
+ Use the JSON output for decisions. Refuse and send the user back to `$hive-lite-discovery` if validation is not valid, `status` is not `ready_for_hive`, or `fields.blockingQuestions` exists and is not `None`.
183
183
 
184
184
  For a valid handoff:
185
185
 
186
- - Set `originalRequirement` from the Start Intent Handoff title plus `fields.agreedDirection`; keep `fields.originalIdea` as provenance only.
187
- - Set `findIntent` from `show.findIntent` when present. If unavailable, derive it from `Agreed Direction`, `User-Confirmed Decisions`, `Non-Goals`, `Acceptance Signals`, and `Unknowns For Hive Lite To Resolve`.
186
+ - Set `originalRequirement` from the Start Intent Handoff title, `fields.agreedDirection`, and `fields.userConfirmedDecisions`; keep `fields.originalIdea` as provenance only.
187
+ - Set `findIntent` from `show.findIntent` when present. If unavailable, derive it only from `Agreed Direction`.
188
+ - Treat `User-Confirmed Decisions` as coding-prompt requirement context, not routing text for `hive-lite find`.
188
189
  - Treat `Pre-Hive Observations / Hints` as non-authoritative hints only. Hive Lite must verify them through Project Map and Context Packet discovery.
189
190
  - Do not add Direct Writable files, final file scope, implementation plan, validation plan, or files to modify from the handoff. Those are execution boundaries produced later by `hive-lite find`.
190
191
  - Do not ask new discovery questions inside this skill. If product questions are still unresolved, stop with an explicit `$hive-lite-discovery` handoff.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hive-lite",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Local Project Map + Change Control for coding agents.",
5
5
  "keywords": [
6
6
  "codex",
package/src/cli.js CHANGED
@@ -80,8 +80,8 @@ Usage:
80
80
  hive-lite intent save --stdin --json
81
81
  hive-lite intent validate <intent_id_or_path> --json
82
82
  hive-lite intent show <intent_id_or_path> --json
83
- hive-lite operator run "<clear requirement>" [--operator-agent-command "..."] [--operator-agent-mode stdin|prompt_file]
84
- hive-lite operator run --intent <intent_id_or_path> [--operator-agent-command "..."] [--operator-agent-mode stdin|prompt_file]
83
+ hive-lite operator run "<clear requirement>" [--operator-agent-command "..."] [--operator-agent-mode stdin|prompt_file] [--operator-agent-timeout-ms 600000]
84
+ hive-lite operator run --intent <intent_id_or_path> [--operator-agent-command "..."] [--operator-agent-mode stdin|prompt_file] [--operator-agent-timeout-ms 600000]
85
85
  hive-lite find "<intent>" [--area <id>] [--max-files 8] [--explain] [--json]
86
86
  hive-lite check [changeId] [--context ctx_id] [--json]
87
87
  hive-lite validate [changeId] [--cmd "..."] [--profile <id>]
@@ -997,6 +997,7 @@ async function main(argv) {
997
997
  intent: parsed.flags.intent || null,
998
998
  operatorAgentCommand: parsed.flags.operatorAgentCommand || parsed.flags.agentCommand,
999
999
  operatorAgentMode: parsed.flags.operatorAgentMode || parsed.flags.agentMode,
1000
+ operatorAgentTimeoutMs: parsed.flags.operatorAgentTimeoutMs || parsed.flags.agentTimeoutMs,
1000
1001
  }, {
1001
1002
  stdin: process.stdin,
1002
1003
  stdout: process.stdout,
package/src/lib/intent.js CHANGED
@@ -6,41 +6,47 @@ const { parseYaml } = require('./yaml');
6
6
  const SCHEMA_VERSION = 'hive.start_intent.v1';
7
7
  const INTENT_ID_PATTERN = /^int_[a-z0-9][a-z0-9_-]*$/;
8
8
 
9
- const REQUIRED_SECTIONS = [
9
+ const INTENT_SECTIONS = [
10
10
  {
11
11
  key: 'originalIdea',
12
12
  title: 'Original Idea',
13
13
  aliases: ['original idea'],
14
+ required: true,
14
15
  allowNone: false,
15
16
  },
16
17
  {
17
18
  key: 'agreedDirection',
18
19
  title: 'Agreed Direction',
19
20
  aliases: ['agreed direction'],
21
+ required: true,
20
22
  allowNone: false,
21
23
  },
22
24
  {
23
25
  key: 'userConfirmedDecisions',
24
26
  title: 'User-Confirmed Decisions',
25
27
  aliases: ['user confirmed decisions', 'user-confirmed decisions'],
28
+ required: true,
26
29
  allowNone: true,
27
30
  },
28
31
  {
29
32
  key: 'nonGoals',
30
33
  title: 'Non-Goals',
31
34
  aliases: ['non goals', 'non-goals'],
35
+ required: false,
32
36
  allowNone: false,
33
37
  },
34
38
  {
35
39
  key: 'acceptanceSignals',
36
40
  title: 'Acceptance Signals',
37
41
  aliases: ['acceptance signals'],
42
+ required: false,
38
43
  allowNone: false,
39
44
  },
40
45
  {
41
46
  key: 'unknownsForHiveLiteToResolve',
42
47
  title: 'Unknowns For Hive Lite To Resolve',
43
48
  aliases: ['unknowns for hive lite to resolve'],
49
+ required: false,
44
50
  allowNone: false,
45
51
  },
46
52
  {
@@ -54,18 +60,20 @@ const REQUIRED_SECTIONS = [
54
60
  'pre hive hints',
55
61
  'pre-hive hints',
56
62
  ],
63
+ required: false,
57
64
  allowNone: true,
58
65
  },
59
66
  {
60
67
  key: 'blockingQuestions',
61
68
  title: 'Blocking Questions',
62
69
  aliases: ['blocking questions'],
70
+ required: false,
63
71
  allowNone: true,
64
72
  },
65
73
  ];
66
74
 
67
75
  const SECTION_BY_NORMALIZED = new Map();
68
- for (const section of REQUIRED_SECTIONS) {
76
+ for (const section of INTENT_SECTIONS) {
69
77
  for (const alias of section.aliases) SECTION_BY_NORMALIZED.set(normalizeHeading(alias), section);
70
78
  }
71
79
 
@@ -286,7 +294,7 @@ function validateSections(parsed, frontmatter, errors, warnings) {
286
294
  }
287
295
  }
288
296
 
289
- for (const required of REQUIRED_SECTIONS) {
297
+ for (const required of INTENT_SECTIONS.filter((section) => section.required)) {
290
298
  const section = parsed.sections[required.key];
291
299
  if (!section) {
292
300
  addError(errors, 'MISSING_SECTION', `Missing required section: ${required.title}.`, { section: required.title });
@@ -307,11 +315,6 @@ function validateSections(parsed, frontmatter, errors, warnings) {
307
315
  const unknowns = parsed.sections.unknownsForHiveLiteToResolve;
308
316
  if (unknowns) {
309
317
  const items = contentItems(unknowns);
310
- if (items.length === 0) {
311
- addError(errors, 'EMPTY_CODEBASE_UNKNOWNS', 'Unknowns For Hive Lite To Resolve must list codebase/context unknowns.', {
312
- section: 'Unknowns For Hive Lite To Resolve',
313
- });
314
- }
315
318
  for (const item of items) {
316
319
  if (PRODUCT_DECISION_PATTERN.test(item)) {
317
320
  addError(errors, 'PRODUCT_DECISION_IN_UNKNOWNS', 'Product decisions belong in User-Confirmed Decisions or Blocking Questions, not Unknowns For Hive Lite To Resolve.', {
@@ -367,10 +370,6 @@ function validateExecutionContent(markdown, parsed, errors, warnings) {
367
370
  function deriveFindIntent(fields) {
368
371
  const parts = [
369
372
  ['Agreed Direction', fields.agreedDirection],
370
- ['User-Confirmed Decisions', fields.userConfirmedDecisions],
371
- ['Non-Goals', fields.nonGoals],
372
- ['Acceptance Signals', fields.acceptanceSignals],
373
- ['Codebase Unknowns For Hive Lite To Resolve', fields.unknownsForHiveLiteToResolve],
374
373
  ].filter(([, value]) => value && !isNone(value));
375
374
 
376
375
  return parts.map(([label, value]) => `${label}:\n${value}`).join('\n\n');
@@ -380,7 +379,7 @@ function parseStartIntentMarkdown(markdown) {
380
379
  const front = parseFrontmatter(markdown);
381
380
  const parsed = parseMarkdownSections(front.body);
382
381
  const fields = {};
383
- for (const section of REQUIRED_SECTIONS) {
382
+ for (const section of INTENT_SECTIONS) {
384
383
  fields[section.key] = parsed.sections[section.key] ? parsed.sections[section.key].content : '';
385
384
  }
386
385
 
@@ -49,8 +49,11 @@ const ARTIFACT_FILENAMES = {
49
49
  review_packet_path: 'review-packet.md',
50
50
  handoff_prompt_path: 'handoff-prompt.md',
51
51
  start_intent_path: 'start-intent.md',
52
+ map_update_required_path: 'map-update-required.md',
52
53
  };
53
54
 
55
+ const DEFAULT_AGENT_TIMEOUT_MS = 10 * 60 * 1000;
56
+
54
57
  function rel(root, file) {
55
58
  return path.relative(root, file).replace(/\\/g, '/');
56
59
  }
@@ -99,6 +102,19 @@ function splitCommand(commandText) {
99
102
  return parts;
100
103
  }
101
104
 
105
+ function parsePositiveInteger(value, label) {
106
+ if (value === undefined || value === null || value === '') return null;
107
+ const parsed = Number(value);
108
+ if (!Number.isInteger(parsed) || parsed < 0) {
109
+ throw new Error(`${label} must be a non-negative integer number of milliseconds.`);
110
+ }
111
+ return parsed;
112
+ }
113
+
114
+ function firstConfigured(values) {
115
+ return values.find((value) => value !== undefined && value !== null && value !== '');
116
+ }
117
+
102
118
  function resolveAgentConfig(options = {}, env = process.env) {
103
119
  const commandText = options.operatorAgentCommand
104
120
  || options.agentCommand
@@ -120,11 +136,25 @@ function resolveAgentConfig(options = {}, env = process.env) {
120
136
  if (!['stdin', 'prompt_file'].includes(mode)) {
121
137
  throw new Error(`Unsupported operator agent mode: ${mode}. Use stdin or prompt_file.`);
122
138
  }
139
+ const commandName = path.basename(argv[0]);
140
+ if (mode === 'stdin' && commandName === 'codex' && !argv.slice(1).some((arg) => arg === 'exec' || arg === 'e')) {
141
+ throw new Error('Codex interactive CLI requires a terminal when stdin is piped. Use HIVE_LITE_OPERATOR_AGENT_COMMAND="codex -a never -s workspace-write exec" with HIVE_LITE_OPERATOR_AGENT_MODE="stdin", or pass --operator-agent-command "codex -a never -s workspace-write exec".');
142
+ }
143
+ const timeoutMs = parsePositiveInteger(
144
+ firstConfigured([
145
+ options.operatorAgentTimeoutMs,
146
+ options.agentTimeoutMs,
147
+ env.HIVE_LITE_OPERATOR_AGENT_TIMEOUT_MS,
148
+ env.HIVE_OPERATOR_AGENT_TIMEOUT_MS,
149
+ ]),
150
+ 'operator agent timeout',
151
+ ) ?? DEFAULT_AGENT_TIMEOUT_MS;
123
152
  return {
124
153
  commandText,
125
154
  command: argv[0],
126
155
  args: argv.slice(1),
127
156
  mode,
157
+ timeoutMs,
128
158
  };
129
159
  }
130
160
 
@@ -305,6 +335,26 @@ function baseInvocation(run, paths, skill, body) {
305
335
  '- accepted',
306
336
  '- done',
307
337
  '',
338
+ 'Required result shapes:',
339
+ '- For `map_update_required`, write a human-readable artifact first, then write `step-result.json` like:',
340
+ '```json',
341
+ JSON.stringify({
342
+ schema_version: STEP_RESULT_SCHEMA_VERSION,
343
+ skill,
344
+ status: 'map_update_required',
345
+ recommended_action: 'map_update_required',
346
+ artifacts: {
347
+ map_update_required_path: rel(run.root, path.join(paths.artifactsDir, 'map-update-required.md')),
348
+ },
349
+ map_update: {
350
+ changed_files: ['.hive/map/areas.yaml'],
351
+ reason: 'Project Map changed and must be committed or stashed before continuing.',
352
+ },
353
+ }, null, 2),
354
+ '```',
355
+ '- For `blocked`, write `recommended_action: revise_required` and include `blockers` with concrete next steps.',
356
+ '- For `failed`, use only unexpected tool/runtime failures that prevent a normal structured result.',
357
+ '',
308
358
  'Artifact paths in step-result.json must be under .hive/state/operator/.',
309
359
  '',
310
360
  ].join('\n');
@@ -506,6 +556,20 @@ function runSubprocess(command, args, options) {
506
556
  const stdout = fs.createWriteStream(options.stdoutPath);
507
557
  const stderr = fs.createWriteStream(options.stderrPath);
508
558
  let spawnError = null;
559
+ let timedOut = false;
560
+ let killTimer = null;
561
+ const timeoutMs = Number.isInteger(options.timeoutMs) ? options.timeoutMs : DEFAULT_AGENT_TIMEOUT_MS;
562
+ const timeoutTimer = timeoutMs > 0
563
+ ? setTimeout(() => {
564
+ timedOut = true;
565
+ child.kill('SIGTERM');
566
+ killTimer = setTimeout(() => {
567
+ child.kill('SIGKILL');
568
+ }, Math.min(2000, timeoutMs));
569
+ if (killTimer.unref) killTimer.unref();
570
+ }, timeoutMs)
571
+ : null;
572
+ if (timeoutTimer && timeoutTimer.unref) timeoutTimer.unref();
509
573
  child.on('error', (error) => {
510
574
  spawnError = error;
511
575
  });
@@ -517,9 +581,11 @@ function runSubprocess(command, args, options) {
517
581
  child.stdin.end();
518
582
  }
519
583
  child.on('close', (code, signal) => {
584
+ if (timeoutTimer) clearTimeout(timeoutTimer);
585
+ if (killTimer) clearTimeout(killTimer);
520
586
  stdout.end();
521
587
  stderr.end();
522
- resolve({ code, signal, error: spawnError });
588
+ resolve({ code, signal, error: spawnError, timedOut, timeoutMs });
523
589
  });
524
590
  });
525
591
  }
@@ -545,6 +611,7 @@ async function runAgentStep(run, baseStep, skill, input, agentConfig, io) {
545
611
  }
546
612
 
547
613
  io.stdout.write('[OK] agent session started\n');
614
+ io.stdout.write(`[..] waiting for structured result (logs: ${rel(run.root, paths.stdoutPath)}, ${rel(run.root, paths.stderrPath)})\n`);
548
615
  const before = statusSnapshot(run.root);
549
616
  const proc = await runSubprocess(agentConfig.command, args, {
550
617
  cwd: run.root,
@@ -554,8 +621,8 @@ async function runAgentStep(run, baseStep, skill, input, agentConfig, io) {
554
621
  resultPath: paths.resultPath,
555
622
  artifactsDir: paths.artifactsDir,
556
623
  runId: run.id,
624
+ timeoutMs: agentConfig.timeoutMs,
557
625
  });
558
- io.stdout.write('[..] waiting for result\n');
559
626
 
560
627
  const after = statusSnapshot(run.root);
561
628
  validateSkillAuthority(run.root, skill, before, after);
@@ -563,6 +630,9 @@ async function runAgentStep(run, baseStep, skill, input, agentConfig, io) {
563
630
  if (proc.error) {
564
631
  throw new Error(`Operator agent failed to start: ${proc.error.message}`);
565
632
  }
633
+ if (proc.timedOut) {
634
+ throw new Error(`Operator agent timed out after ${proc.timeoutMs}ms before completing the step. Transcript logs: ${rel(run.root, paths.stdoutPath)}, ${rel(run.root, paths.stderrPath)}. Expected result: ${rel(run.root, paths.resultPath)}`);
635
+ }
566
636
  if (proc.code !== 0) {
567
637
  throw new Error(`Operator agent exited with code ${proc.code}. Transcript logs: ${rel(run.root, paths.stdoutPath)}, ${rel(run.root, paths.stderrPath)}`);
568
638
  }
@@ -597,7 +667,7 @@ function validateArtifactHashes(root, result, pathFields) {
597
667
 
598
668
  function validateAndLoadStepResult(run, paths, expectedSkill) {
599
669
  if (!exists(paths.resultPath)) {
600
- throw new Error(`Operator step failed: missing step-result.json at ${rel(run.root, paths.resultPath)}`);
670
+ throw new Error(`Operator step failed: missing step-result.json at ${rel(run.root, paths.resultPath)}. Transcript logs: ${rel(run.root, paths.stdoutPath)}, ${rel(run.root, paths.stderrPath)}`);
601
671
  }
602
672
  let result;
603
673
  try {
@@ -668,6 +738,10 @@ function validateRequiredArtifacts(result) {
668
738
  throw new Error('Operator step failed: split_required requires split.ready_phase.handoff_prompt_path');
669
739
  }
670
740
  }
741
+ if (result.status === 'map_update_required') {
742
+ const mapUpdatePath = result.artifacts && result.artifacts.map_update_required_path;
743
+ if (!mapUpdatePath) throw new Error('Operator step failed: map_update_required requires artifacts.map_update_required_path');
744
+ }
671
745
  }
672
746
 
673
747
  function syncCurrentArtifacts(root, result, resultPath) {
@@ -947,8 +1021,11 @@ async function renderBlocked(run, result, agentConfig, input, io) {
947
1021
  return runAgentStep(run, 'finish', SKILLS.finish, {}, agentConfig, io);
948
1022
  }
949
1023
 
950
- async function renderMapUpdateRequired(result, input, io) {
1024
+ async function renderMapUpdateRequired(run, result, input, io) {
951
1025
  io.stdout.write('\nMap Update Required\n\n');
1026
+ const details = displayArtifact(run.root, result, 'map_update_required_path');
1027
+ io.stdout.write('Details\n');
1028
+ io.stdout.write(` ${details}\n\n`);
952
1029
  const changed = result.map_update && Array.isArray(result.map_update.changed_files)
953
1030
  ? result.map_update.changed_files
954
1031
  : [];
@@ -960,10 +1037,22 @@ async function renderMapUpdateRequired(result, input, io) {
960
1037
  io.stdout.write('Reason\n');
961
1038
  io.stdout.write(' Project Map was updated while preparing this requirement.\n');
962
1039
  io.stdout.write(' Commit or stash the map update before generating the coding prompt.\n\n');
963
- io.stdout.write('Actions\n');
964
- io.stdout.write(' [q] Quit\n');
965
- await input.ask('> ', 'q');
966
- return null;
1040
+ for (;;) {
1041
+ io.stdout.write('Actions\n');
1042
+ io.stdout.write(' [o] Open details\n');
1043
+ io.stdout.write(' [q] Quit\n');
1044
+ const choice = (await input.ask('> ', 'q')).trim().toLowerCase();
1045
+ if (choice === 'o') {
1046
+ const file = artifactPath(run.root, result, 'map_update_required_path');
1047
+ const text = readText(file);
1048
+ io.stdout.write('\n');
1049
+ io.stdout.write(text);
1050
+ if (!text.endsWith('\n')) io.stdout.write('\n');
1051
+ io.stdout.write('\n');
1052
+ continue;
1053
+ }
1054
+ return null;
1055
+ }
967
1056
  }
968
1057
 
969
1058
  async function handleStepResult(run, result, agentConfig, input, io) {
@@ -984,7 +1073,7 @@ async function handleStepResult(run, result, agentConfig, input, io) {
984
1073
  } else if (current.status === 'blocked' || current.recommended_action === 'revise_required') {
985
1074
  current = await renderBlocked(run, current, agentConfig, input, io);
986
1075
  } else if (current.status === 'map_update_required') {
987
- current = await renderMapUpdateRequired(current, input, io);
1076
+ current = await renderMapUpdateRequired(run, current, input, io);
988
1077
  } else if (current.status === 'accepted' || current.status === 'done') {
989
1078
  io.stdout.write('\nOperator step complete.\n');
990
1079
  current = null;