acstack 0.50.1 → 0.50.3

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "acstack",
3
3
  "description": "Agentic coding stack where planning grows only as far as needed: a map of decisions, tasks, checks, and handoffs behind a pluggable tracker.",
4
- "version": "0.50.1",
4
+ "version": "0.50.3",
5
5
  "author": {
6
6
  "name": "ac8318740"
7
7
  },
package/README.md CHANGED
@@ -91,6 +91,9 @@ Then, in your project, run `/acstack:setup`.
91
91
  - Freshness uses `/acstack:handoff` when a fresh window or independent read helps more
92
92
  - With no thresholds set, the first nudge comes at 200k tokens or half the window, whichever is lower
93
93
  - The urgent nudge comes at 500k tokens or 80% of the window, whichever is lower
94
+ - ACStack includes the question-status bridge automatically
95
+ - With `@juicesharp/rpiv-ask-user-question` and Herdr's Pi integration installed, Herdr shows blocked while Pi waits for an answer
96
+ - Existing setup-installed bridges can stay installed without counting the same wait twice
94
97
  - Both subagent packages find the four agents through the `pi.subagents` field in `package.json`
95
98
  - `@tintinweb/pi-subagents` exposes `Agent`; `pi-subagents` exposes `subagent`
96
99
  - The agents use your subagent package's default model
@@ -303,6 +303,9 @@ PY
303
303
 
304
304
  local integ; integ=$(cfg_get herdr.integration none)
305
305
  if [ "$integ" != "none" ] && [ -n "$integ" ]; then
306
- herdr integration install "$integ" >/dev/null 2>&1 && say "herdr $integ state hook installed"
306
+ if herdr integration install "$integ" >/dev/null 2>&1; then
307
+ say "herdr $integ state hook installed"
308
+ patch_herdr_pi_status
309
+ fi
307
310
  fi
308
311
  }
@@ -189,6 +189,7 @@ PY
189
189
  say "delta unset as git pager"
190
190
  say "now set delta.enabled: false in $CFG so apply does not restore it" ;;
191
191
  herdr)
192
+ patch_herdr_pi_status --remove
192
193
  py "$HERDR_CFG" "$BEGIN" "$END" <<'PY'
193
194
  import re, sys, os
194
195
  p, b, e = sys.argv[1:4]
@@ -0,0 +1,86 @@
1
+ """Keep herdr's single Pi status writer aware of background subagents."""
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ path = Path(sys.argv[1])
8
+ remove = sys.argv[2:] == ["--remove"]
9
+ if not path.exists():
10
+ sys.exit(0)
11
+ if path.is_symlink() or not path.is_file():
12
+ sys.exit(" pi: herdr status patch skipped: reporter is not a regular file")
13
+
14
+ begin = "// >>> acstack terminal-workspace: background status >>>"
15
+ end = "// <<< acstack terminal-workspace: background status <<<"
16
+
17
+
18
+ def marked(text):
19
+ return f" {begin}\n{text} {end}\n"
20
+
21
+
22
+ replacements = [
23
+ (" let agentActive = false;\n", " let agentActive = false;\n" + marked(''' // shortcut: no workflow/reload replay; extend when Tintin exposes active-run snapshots.
24
+ const backgroundAgents = new Set<string>();
25
+ let busyCount = 0;
26
+ ''')),
27
+ (" if (agentActive) {\n", marked(''' // blocked > active parent or children > idle
28
+ if (agentActive || backgroundAgents.size > 0 || busyCount > 0) {
29
+ ''')),
30
+ (' pi.events.on("herdr:blocked", (data) => {\n', marked(''' // Tintin's in-process children share this bus, but never own the pane status.
31
+ pi.events.on("subagents:started", (data) => {
32
+ if (!rootSession || typeof data?.id !== "string" || !data.id) return;
33
+ backgroundAgents.add(data.id);
34
+ publishState();
35
+ });
36
+ for (const event of ["subagents:completed", "subagents:failed"]) {
37
+ pi.events.on(event, (data) => {
38
+ if (!rootSession || typeof data?.id !== "string") return;
39
+ backgroundAgents.delete(data.id);
40
+ publishState();
41
+ });
42
+ }
43
+ // nicobailon/pi-subagents uses balanced active intervals instead of IDs.
44
+ pi.events.on("herdr:busy", (data) => {
45
+ if (!rootSession || typeof data?.active !== "boolean") return;
46
+ busyCount = Math.max(0, busyCount + (data.active ? 1 : -1));
47
+ publishState();
48
+ });
49
+ ''') + ' pi.events.on("herdr:blocked", (data) => {\n'),
50
+ ]
51
+
52
+ text = path.read_text()
53
+ if not text.startswith("// installed by herdr\n") or "HERDR_INTEGRATION_ID=pi\n" not in text:
54
+ sys.exit(" pi: herdr status patch skipped: unrecognised reporter")
55
+ # These lifecycle guards must still exist before adding shared-bus listeners.
56
+ required = [
57
+ ' pi.on("agent_settled", (_event, ctx) => {',
58
+ ' if (ctx?.mode !== "tui") {',
59
+ ' if (!rootSession || ctx?.isIdle?.() !== true) {',
60
+ ]
61
+ if any(text.count(anchor) != 1 for anchor in required):
62
+ sys.exit(" pi: herdr status patch skipped: upstream lifecycle changed")
63
+ patched = begin in text
64
+ if patched == (not remove):
65
+ # Validate every managed block before treating an existing patch as ours.
66
+ if patched and any(text.count(new) != 1 for _, new in replacements):
67
+ sys.exit(" pi: herdr status patch skipped: managed blocks were changed")
68
+ sys.exit(0)
69
+
70
+ for old, new in replacements:
71
+ if text.count(new if remove else old) != 1:
72
+ sys.exit(" pi: herdr status patch skipped: upstream layout changed")
73
+ for old, new in replacements:
74
+ text = text.replace(new, old, 1) if remove else text.replace(old, new, 1)
75
+
76
+ # Validate all matches first, then replace atomically so a failed patch leaves no partial writer.
77
+ fd, temporary = tempfile.mkstemp(dir=path.parent, prefix=".acstack-herdr-")
78
+ try:
79
+ with os.fdopen(fd, "w") as stream:
80
+ stream.write(text)
81
+ os.chmod(temporary, path.stat().st_mode)
82
+ os.replace(temporary, path)
83
+ finally:
84
+ if os.path.exists(temporary):
85
+ os.unlink(temporary)
86
+ print(f" pi: herdr background status patch {'removed' if remove else 'applied'}")
@@ -301,6 +301,7 @@ apply_pi() {
301
301
  pi_state merge "$PI_SUBAGENT_CFG" subagent '{"toolActivation": "eager"}'
302
302
  pi_state md write
303
303
  patch_pi_win32_paste
304
+ patch_herdr_pi_status
304
305
 
305
306
  # Logging in is interactive and stores a credential, so it stays the user's.
306
307
  [ -f "$PI_DIR/auth.json" ] || say "pi: not logged in yet. Run pi, then /login"
@@ -311,6 +312,7 @@ apply_pi() {
311
312
  # and leaves the pi binary, its logins and its sessions alone.
312
313
  remove_pi() {
313
314
  find_pi
315
+ patch_herdr_pi_status --remove
314
316
  pi_question_bridge remove
315
317
  if pi_state parses "$PI_SETTINGS"; then
316
318
  pi_unpin "$PI_ACSTACK"
@@ -404,6 +404,12 @@ find_pi() {
404
404
  }
405
405
 
406
406
 
407
+ # Keep herdr's own writer as the sole pane authority; reapply after it is replaced.
408
+ patch_herdr_pi_status() {
409
+ python3 "$SCRIPT_DIR/lib/patch-herdr-pi.py" "$PI_DIR/extensions/herdr-agent-state.ts" "$@" \
410
+ || say "pi: herdr background status patch was not applied; reporter left unchanged"
411
+ }
412
+
407
413
  # Pi's bundled TUI does not always receive Windows Terminal paste as plain
408
414
  # text. Through Herdr and ConPTY it can arrive as Win32 input records such as
409
415
  # ESC[13;28;13;1;0;1_. Patch the installed bundle after install or update.
@@ -204,8 +204,12 @@ upgrade_one() { # upgrade_one <name>
204
204
  find_pi
205
205
  fi
206
206
  case "$1" in
207
- herdr) herdr update >/dev/null 2>&1 \
208
- && say "herdr updated" || say "herdr update failed" ;;
207
+ herdr) if herdr update >/dev/null 2>&1; then
208
+ say "herdr updated"
209
+ patch_herdr_pi_status
210
+ else
211
+ say "herdr update failed"
212
+ fi ;;
209
213
  gh-dash) gh extension upgrade dlvhdr/gh-dash >/dev/null 2>&1 \
210
214
  && say "gh-dash upgraded" || say "gh-dash upgrade failed" ;;
211
215
  harlequin) uv tool upgrade harlequin >/dev/null 2>&1 \
@@ -1,31 +1,19 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
- export default function (pi: ExtensionAPI) {
4
- let pending: { active: boolean; native: boolean } | undefined;
5
- let forwarding = false;
6
-
7
- pi.events.on("herdr:blocked", (event: unknown) => {
8
- if (!forwarding && pending && (event as { active?: unknown } | null)?.active === pending.active) {
9
- pending.native = true;
10
- pending = undefined;
11
- }
12
- });
3
+ const TAKEOVER = "acstack:question-blocked:takeover";
13
4
 
14
- pi.events.on("rpiv:ask-user:blocked", (event: unknown) => {
15
- const active = (event as { active?: unknown } | null)?.active;
16
- if (typeof active !== "boolean") return;
17
- const transition = { active, native: false };
18
- pending = transition;
19
- // shortcut: dual producers emit public then herdr synchronously; async pairs need an upstream source ID
20
- queueMicrotask(() => {
21
- if (pending === transition) pending = undefined;
22
- if (transition.native) return;
23
- forwarding = true;
24
- try {
25
- pi.events.emit("herdr:blocked", { active });
26
- } finally {
27
- forwarding = false;
28
- }
5
+ export default function (pi: ExtensionAPI) {
6
+ pi.on("session_start", () => {
7
+ pi.events.emit(TAKEOVER, {});
8
+ // Register after legacy factories so our synchronous relay suppresses their queued copy.
9
+ // shortcut: legacy compatibility excludes recursively emitted public events; remove the legacy copy if needed
10
+ const stopRelay = pi.events.on("rpiv:ask-user:blocked", (event: unknown) => {
11
+ const active = (event as { active?: unknown } | null)?.active;
12
+ if (typeof active === "boolean") pi.events.emit("herdr:blocked", { active });
13
+ });
14
+ const stopTakeover = pi.events.on(TAKEOVER, () => {
15
+ stopRelay();
16
+ stopTakeover();
29
17
  });
30
18
  });
31
19
  }
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acstack-cli",
3
- "version": "0.50.1",
3
+ "version": "0.50.3",
4
4
  "description": "CLI for the ACStack agentic coding stack",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -365,11 +365,14 @@ What `apply` does:
365
365
  - ACStack uses it because decisions can include markdown, code and artifact previews
366
366
  - It accepts freeform answers and notes, and needs no full-event telemetry environment variable
367
367
  - This setup still keeps pi's install telemetry disabled through `pi.settings.install_telemetry: false`
368
- - Setup installs a managed question bridge under pi's agent directory, honoring `PI_CODING_AGENT_DIR`
368
+ - ACStack's Pi package loads the question bridge automatically, including installations made without terminal-workspace setup
369
+ - Setup also installs a fallback copy under pi's agent directory, honoring `PI_CODING_AGENT_DIR`
370
+ - The packaged bridge and fallback copy count each wait once when both load
369
371
  - The bridge translates `rpiv:ask-user:blocked` into `herdr:blocked`, so herdr shows the question's input wait
370
372
  - Answer, cancellation, and error completion release the question's blocker while other herdr blockers stay active
371
- - A producer that also emits the matching herdr event synchronously contributes only one blocker
372
- - Events carry no source ID, so the bridge treats a matching synchronous herdr event as the producer's own
373
+ - Anonymous native Herdr events remain independent from question events, including simultaneous waits
374
+ - A producer emitting both event types contributes two balanced blockers, which clear when both sources finish
375
+ - Older fallback copies work unless another extension recursively emits public question events; remove the older copy in that case
373
376
  - Reapply refreshes the owned bridge, but preserves a pre-existing file at the same path
374
377
  - `pi.question_tool: false`, `disable pi`, and `uninstall` remove only the owned bridge and leave user workarounds alone
375
378
  - Set `pi.question_tool: false` to leave the package out, or remove it later if this setup installed it
@@ -393,6 +396,14 @@ What `apply` does:
393
396
  - It writes the four keys under `pi.settings` into `~/.pi/agent/settings.json`
394
397
  - It patches pi's bundled TUI so Windows Terminal Win32 input records paste as text
395
398
  - `pi update` can replace the bundle, so `setup.sh upgrade pi` reapplies the patch
399
+ - It patches an existing herdr Pi reporter to keep the parent working while background subagents run
400
+ - Questions still show blocked, even when children are active
401
+ - The patch handles `pi-subagents` busy events and `@tintinweb/pi-subagents` Agent lifecycle events
402
+ - Tintin workflows and children already running during an extension reload lack lifecycle replay and remain outside this tracking
403
+ - `apply`, herdr integration installation, and `setup.sh upgrade herdr` reapply the patch
404
+ - Direct `herdr integration install pi` overwrites the patch; run `apply` afterward, then restart Pi or use `/reload`
405
+ - Setup leaves unknown reporter layouts unchanged and reports the skipped patch
406
+ - `disable pi`, `disable herdr`, and `uninstall` remove unchanged managed patch blocks
396
407
 
397
408
  How it treats pi's files:
398
409
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acstack",
3
- "version": "0.50.1",
3
+ "version": "0.50.3",
4
4
  "private": false,
5
5
  "description": "Agentic coding stack with first-class Claude Code and pi support: skills, orchestrator instructions, and verification agents.",
6
6
  "license": "MIT",
@@ -38,7 +38,8 @@
38
38
  ],
39
39
  "pi": {
40
40
  "extensions": [
41
- "./pi/extension.ts"
41
+ "./pi/extension.ts",
42
+ "./assets/terminal-workspace/pi/acstack-question-blocked.ts"
42
43
  ],
43
44
  "subagents": {
44
45
  "agents": [