instar 1.2.83 → 1.3.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.
Files changed (42) hide show
  1. package/dist/commands/init.js +14 -3
  2. package/dist/commands/init.js.map +1 -1
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +86 -4
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/core/PostUpdateMigrator.d.ts +2 -1
  7. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  8. package/dist/core/PostUpdateMigrator.js +279 -12
  9. package/dist/core/PostUpdateMigrator.js.map +1 -1
  10. package/dist/core/installCodexHooks.d.ts.map +1 -1
  11. package/dist/core/installCodexHooks.js +3 -2
  12. package/dist/core/installCodexHooks.js.map +1 -1
  13. package/dist/scaffold/templates.d.ts.map +1 -1
  14. package/dist/scaffold/templates.js +1 -8
  15. package/dist/scaffold/templates.js.map +1 -1
  16. package/dist/server/routes.d.ts.map +1 -1
  17. package/dist/server/routes.js +28 -67
  18. package/dist/server/routes.js.map +1 -1
  19. package/dist/server/stopGate.d.ts +8 -2
  20. package/dist/server/stopGate.d.ts.map +1 -1
  21. package/dist/server/stopGate.js +42 -2
  22. package/dist/server/stopGate.js.map +1 -1
  23. package/dist/threadline/CollaborationSurfacer.d.ts.map +1 -1
  24. package/dist/threadline/CollaborationSurfacer.js +7 -3
  25. package/dist/threadline/CollaborationSurfacer.js.map +1 -1
  26. package/dist/threadline/hubCommands.d.ts +67 -0
  27. package/dist/threadline/hubCommands.d.ts.map +1 -0
  28. package/dist/threadline/hubCommands.js +126 -0
  29. package/dist/threadline/hubCommands.js.map +1 -0
  30. package/package.json +1 -1
  31. package/playbook-scripts/build-state.py +39 -1
  32. package/scripts/analyze-release.js +16 -8
  33. package/scripts/generate-builtin-manifest.cjs +2 -1
  34. package/src/data/builtin-manifest.json +74 -65
  35. package/src/scaffold/templates.ts +1 -8
  36. package/src/templates/hooks/build-stop-hook.sh +62 -0
  37. package/src/templates/hooks/settings-template.json +10 -0
  38. package/upgrades/1.3.0.md +27 -0
  39. package/upgrades/1.3.1.md +27 -0
  40. package/upgrades/side-effects/build-stop-hook-session-scoping.md +133 -0
  41. package/upgrades/side-effects/fresh-session-stop-gate-shadow-wiring.md +35 -0
  42. package/upgrades/side-effects/threadline-open-this-deterministic.md +45 -0
@@ -474,7 +474,7 @@ export declare class PostUpdateMigrator {
474
474
  * Get the content of a named hook template.
475
475
  * Used by init.ts to share canonical hook content without duplication.
476
476
  */
477
- getHookContent(name: 'session-start' | 'compaction-recovery' | 'external-operation-gate' | 'deferral-detector' | 'slopcheck-guard' | 'post-action-reflection' | 'external-communication-guard' | 'scope-coherence-collector' | 'scope-coherence-checkpoint' | 'claim-intercept' | 'claim-intercept-response' | 'telegram-topic-context' | 'response-review' | 'auto-approve-permissions' | 'skill-usage-telemetry' | 'build-stop-hook'): string;
477
+ getHookContent(name: 'session-start' | 'compaction-recovery' | 'external-operation-gate' | 'deferral-detector' | 'slopcheck-guard' | 'post-action-reflection' | 'external-communication-guard' | 'scope-coherence-collector' | 'scope-coherence-checkpoint' | 'claim-intercept' | 'claim-intercept-response' | 'telegram-topic-context' | 'response-review' | 'stop-gate-router' | 'auto-approve-permissions' | 'skill-usage-telemetry' | 'build-stop-hook'): string;
478
478
  /** Public accessor for grounding-before-messaging hook content (used by init.ts) */
479
479
  getGroundingBeforeMessagingPublic(): string;
480
480
  /** Public accessor for convergence-check script content (used by init.ts) */
@@ -542,6 +542,7 @@ export declare class PostUpdateMigrator {
542
542
  private getFreeTextGuardHook;
543
543
  private getClaimInterceptHook;
544
544
  private getResponseReviewHook;
545
+ private getStopGateRouterHook;
545
546
  private getClaimInterceptResponseHook;
546
547
  private getSkillUsageTelemetryHook;
547
548
  private getBuildStopHook;
@@ -1 +1 @@
1
- {"version":3,"file":"PostUpdateMigrator.d.ts","sourceRoot":"","sources":["../../src/core/PostUpdateMigrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAoCH,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAIjC,MAAM,WAAW,eAAe;IAC9B,wBAAwB;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,kCAAkC;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAiB;IAC/B;;;;;;OAMG;IACH,OAAO,CAAC,UAAU,CAAiC;gBAEvC,MAAM,EAAE,cAAc;IAIlC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAItC;;;;;;OAMG;IACG,eAAe,CACnB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,qBAAqB,CAAC;IAIjC,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,kBAAkB;IAO1B;;;;OAIG;IACH,OAAO,CAAC,YAAY;IASpB;;;OAGG;IACH,OAAO,IAAI,eAAe;IAwC1B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,0BAA0B;IAsElC,OAAO,CAAC,0BAA0B;IAmDlC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,kCAAkC;IAwH1C,OAAO,CAAC,uBAAuB;IAwE/B,OAAO,CAAC,4CAA4C;IA+CpD,OAAO,CAAC,yBAAyB;IA6FjC;;;;;;;;;;OAUG;IACG,YAAY,IAAI,OAAO,CAAC,eAAe,CAAC;YA4BhC,uBAAuB;IAkGrC,OAAO,CAAC,0BAA0B;IAkGlC,OAAO,CAAC,0BAA0B;IAkElC,OAAO,CAAC,oBAAoB;IA4G5B,OAAO,CAAC,8BAA8B;IA2EtC;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAaxB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,0BAA0B;IA8BlC;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,4BAA4B;IAwBpC;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,mCAAmC;IA0C3C;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAW5B;;;;;;;;;OASG;IACH,OAAO,CAAC,kBAAkB;IA2B1B;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,OAAO,CAAC,yBAAyB;IA4HjC;6EACyE;IACzE,OAAO,CAAC,wBAAwB;IAShC;sDACkD;IAClD,OAAO,CAAC,wBAAwB;IAQhC;;;;OAIG;IACH,OAAO,CAAC,YAAY;IA6NpB;;;;;;;;OAQG;IACH,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI;IA6CvE;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAgEzB;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAkChC;;;;;;;;;OASG;IACH,OAAO,CAAC,wBAAwB;IAoEhC;;;;;;OAMG;IACH,OAAO,CAAC,oBAAoB;IAiE5B;;;;;OAKG;IACH,OAAO,CAAC,2BAA2B;IA8BnC;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IAwGhC;;;;;;OAMG;IACH,OAAO,CAAC,8BAA8B;IAwDtC,OAAO,CAAC,0BAA0B;IA8DlC;;;OAGG;IACH,OAAO,CAAC,eAAe;IAg0BvB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,kCAAkC;IAkG1C;;;OAGG;IACH,OAAO,CAAC,cAAc;IA6ItB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAqQvB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAqFrB;;;OAGG;IACH;;;OAGG;IACH;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,wBAAwB;IAmEhC,OAAO,CAAC,wBAAwB;IAqChC,OAAO,CAAC,gBAAgB;IAiBxB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,OAAO,CAAC,qBAAqB;IAkE7B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,0BAA0B;IAgDlC;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAkC5B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,kBAAkB;IA2C1B;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,oBAAoB;IAgC5B;;;OAGG;IACH,OAAO,CAAC,aAAa;IAyBrB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAqC9B;;;OAGG;IACH,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,qBAAqB,GAAG,yBAAyB,GAAG,mBAAmB,GAAG,iBAAiB,GAAG,wBAAwB,GAAG,8BAA8B,GAAG,2BAA2B,GAAG,4BAA4B,GAAG,iBAAiB,GAAG,0BAA0B,GAAG,wBAAwB,GAAG,iBAAiB,GAAG,0BAA0B,GAAG,uBAAuB,GAAG,iBAAiB,GAAG,MAAM;IAqB/a,oFAAoF;IACpF,iCAAiC,IAAI,MAAM;IAI3C,6EAA6E;IAC7E,yBAAyB,IAAI,MAAM;IAInC,OAAO,CAAC,mBAAmB;IAoY3B,OAAO,CAAC,wBAAwB;IA8EhC,OAAO,CAAC,2BAA2B;IAoEnC,OAAO,CAAC,yBAAyB;IAuGjC,OAAO,CAAC,2BAA2B;IAqInC,OAAO,CAAC,qBAAqB;IAqP7B,OAAO,CAAC,uBAAuB;IAqJ/B,OAAO,CAAC,qBAAqB;IAsH7B,OAAO,CAAC,2BAA2B;IA8GnC,OAAO,CAAC,iCAAiC;IA6DzC,OAAO,CAAC,4BAA4B;IA4LpC;;;;;;;;;;OAUG;IACH;;;;;;;;;;;OAWG;IAEH,gBAAuB,iCAAiC,EAAE,WAAW,CAAC,MAAM,CAAC,CA4B1E;IAEH;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,8BAA8B;IAiHtC,OAAO,CAAC,uBAAuB;IA4B/B;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,sBAAsB;IAwC9B,OAAO,CAAC,iBAAiB;IAwBzB,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,8BAA8B;IAoHtC,OAAO,CAAC,+BAA+B;IA+JvC,OAAO,CAAC,oBAAoB;IAY5B,OAAO,CAAC,qBAAqB;IA4N7B,OAAO,CAAC,qBAAqB;IA4H7B,OAAO,CAAC,6BAA6B;IAyKrC,OAAO,CAAC,0BAA0B;IAgClC,OAAO,CAAC,gBAAgB;IAqFxB,OAAO,CAAC,6BAA6B;CAoCtC"}
1
+ {"version":3,"file":"PostUpdateMigrator.d.ts","sourceRoot":"","sources":["../../src/core/PostUpdateMigrator.ts"],"names":[],"mappings":"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"}
@@ -1671,6 +1671,13 @@ export class PostUpdateMigrator {
1671
1671
  catch (err) {
1672
1672
  result.errors.push(`response-review.js: ${err instanceof Error ? err.message : String(err)}`);
1673
1673
  }
1674
+ try {
1675
+ fs.writeFileSync(path.join(instarHooksDir, 'stop-gate-router.js'), this.getStopGateRouterHook(), { mode: 0o755 });
1676
+ result.upgraded.push('hooks/instar/stop-gate-router.js (unjustified Stop gate router)');
1677
+ }
1678
+ catch (err) {
1679
+ result.errors.push(`stop-gate-router.js: ${err instanceof Error ? err.message : String(err)}`);
1680
+ }
1674
1681
  try {
1675
1682
  fs.writeFileSync(path.join(instarHooksDir, 'auto-approve-permissions.js'), this.getAutoApprovePermissionsHook(), { mode: 0o755 });
1676
1683
  result.upgraded.push('hooks/instar/auto-approve-permissions.js (subagent permission unblocking)');
@@ -1785,6 +1792,7 @@ export class PostUpdateMigrator {
1785
1792
  'scope-coherence-collector.js', 'scope-coherence-checkpoint.js',
1786
1793
  'instructions-loaded-tracker.js', 'subagent-start-tracker.js',
1787
1794
  'free-text-guard.sh', 'claim-intercept.js', 'claim-intercept-response.js', 'response-review.js',
1795
+ 'stop-gate-router.js',
1788
1796
  'auto-approve-permissions.js',
1789
1797
  ];
1790
1798
  // Check if we're still on the old flat layout (hooks directly in .instar/hooks/)
@@ -2112,15 +2120,23 @@ export class PostUpdateMigrator {
2112
2120
  const stopEntries = hooks.Stop;
2113
2121
  const hasAutonomousHook = stopEntries.some(e => e.hooks?.some(h => h.command?.includes('autonomous-stop-hook')));
2114
2122
  if (!hasAutonomousHook) {
2115
- // Must be first in the Stop chain so it blocks before other hooks run
2116
- hooks.Stop.unshift({
2123
+ // Keep stop-gate-router first when present so shadow telemetry sees every
2124
+ // Stop event before legacy autonomous blocking can short-circuit the chain.
2125
+ const autonomousEntry = {
2117
2126
  matcher: '',
2118
2127
  hooks: [{
2119
2128
  type: 'command',
2120
2129
  command: 'bash ${CLAUDE_PROJECT_DIR}/.claude/skills/autonomous/hooks/autonomous-stop-hook.sh',
2121
2130
  timeout: 10000,
2122
2131
  }],
2123
- });
2132
+ };
2133
+ const stopGateIndex = stopEntries.findIndex(e => e.hooks?.some(h => h.command?.includes('stop-gate-router.js')));
2134
+ if (stopGateIndex >= 0) {
2135
+ stopEntries.splice(stopGateIndex + 1, 0, autonomousEntry);
2136
+ }
2137
+ else {
2138
+ stopEntries.unshift(autonomousEntry);
2139
+ }
2124
2140
  result.upgraded.push('.claude/settings.json: registered autonomous stop hook (structural enforcement)');
2125
2141
  patched = true;
2126
2142
  }
@@ -2307,18 +2323,20 @@ Threadline activity NEVER spawns a new Telegram topic per event. Notices route o
2307
2323
  - A conversation **bound to a parent topic** → its real replies surface THERE (handled automatically).
2308
2324
  - A **parentless** conversation + any **status/housekeeping** notice → a single, SILENT **"Threadline" hub topic**. It does not buzz the user — agent-to-agent chatter isn't the user's job by default; the hub is a calm, browsable record.
2309
2325
 
2310
- When the user is reading the Threadline hub topic and says **"open this"** or **"tie this to <an existing topic>"**, act on it by calling the bind endpoint do NOT just reply inline:
2311
- \`\`\`bash
2312
- # open this → create a fresh topic and bind the conversation (most-recent unbound, or pass threadId)
2313
- curl -X POST -H "Authorization: Bearer $AUTH" -H 'Content-Type: application/json' http://localhost:${port}/threadline/hub/bind -d '{"action":"open"}'
2314
- # tie this to an existing topic
2315
- curl -X POST -H "Authorization: Bearer $AUTH" -H 'Content-Type: application/json' http://localhost:${port}/threadline/hub/bind -d '{"action":"tie","targetTopicId":1234}'
2316
- \`\`\`
2317
- After binding, that conversation's future updates flow to the bound topic automatically. If more than one conversation is unbound, the endpoint returns 409 — ask the user which one (pass its \`threadId\`).
2326
+ When the user is reading the Threadline hub topic and says **"open this"** or **"tie this to <an existing topic>"**, this is handled **structurally** the system intercepts those exact commands in the hub topic and binds the conversation automatically (bare "open this" opens the most-recent one) BEFORE the message reaches me. I will not see "open this" as a message to interpret, and must NOT reply to it conversationally. (Also available as \`POST /threadline/hub/bind\` \`{action:"open"|"tie", ...}\` for scripted use.) After binding, that conversation's future updates flow to the bound topic automatically.
2318
2327
  `;
2319
2328
  content += '\n' + hubSection;
2320
2329
  patched = true;
2321
- result.upgraded.push('CLAUDE.md: added Threadline hub + "open this"/bind guidance (CMT-519)');
2330
+ result.upgraded.push('CLAUDE.md: added Threadline hub + "open this" guidance (CMT-519)');
2331
+ }
2332
+ // CMT-529 — agents migrated under CMT-519 got the OLD "call the bind endpoint"
2333
+ // wording; "open this" is now a STRUCTURAL intercept (handled before the agent).
2334
+ // Re-patch the stale sentence so the agent doesn't try to call the endpoint /
2335
+ // reply to a command it will never actually see.
2336
+ if (content.includes('act on it by calling the bind endpoint')) {
2337
+ content = content.replace(/When the user is reading the Threadline hub topic and says \*\*"open this"\*\*[\s\S]*?(?:returns 409 — ask the user which one \(pass its `threadId`\)\.)/, `When the user is reading the Threadline hub topic and says **"open this"** or **"tie this to <an existing topic>"**, this is handled **structurally** — the system intercepts those exact commands and binds the conversation automatically (bare "open this" opens the most-recent one) BEFORE the message reaches me. I will not see "open this" as a message to interpret, and must NOT reply to it conversationally.`);
2338
+ patched = true;
2339
+ result.upgraded.push('CLAUDE.md: updated "open this" guidance to structural-intercept (CMT-529)');
2322
2340
  }
2323
2341
  // Multi-Session Autonomy awareness (Agent Awareness Standard). Existing
2324
2342
  // agents need to know they can run concurrent per-topic autonomous jobs and
@@ -3544,6 +3562,23 @@ Create worktrees for collaborator repos with \`instar worktree create <branch>\`
3544
3562
  this.migrateSettingsHookPaths(hooks.PostToolUse, result);
3545
3563
  patched = true;
3546
3564
  }
3565
+ {
3566
+ const stopHooks = (hooks.Stop ?? []);
3567
+ const hasStopGateRouter = stopHooks.some(e => e.hooks?.some(h => h.command?.includes('stop-gate-router.js')));
3568
+ if (!hasStopGateRouter) {
3569
+ stopHooks.unshift({
3570
+ matcher: '',
3571
+ hooks: [{
3572
+ type: 'command',
3573
+ command: 'node ${CLAUDE_PROJECT_DIR}/.instar/hooks/instar/stop-gate-router.js',
3574
+ timeout: 5000,
3575
+ }],
3576
+ });
3577
+ hooks.Stop = stopHooks;
3578
+ patched = true;
3579
+ result.upgraded.push('.claude/settings.json: added Stop stop-gate-router hook');
3580
+ }
3581
+ }
3547
3582
  if (hooks.Stop) {
3548
3583
  this.migrateSettingsHookPaths(hooks.Stop, result);
3549
3584
  patched = true;
@@ -4188,6 +4223,7 @@ Create worktrees for collaborator repos with \`instar worktree create <branch>\`
4188
4223
  case 'claim-intercept-response': return this.getClaimInterceptResponseHook();
4189
4224
  case 'telegram-topic-context': return this.getTelegramTopicContextHook();
4190
4225
  case 'response-review': return this.getResponseReviewHook();
4226
+ case 'stop-gate-router': return this.getStopGateRouterHook();
4191
4227
  case 'auto-approve-permissions': return this.getAutoApprovePermissionsHook();
4192
4228
  case 'skill-usage-telemetry': return this.getSkillUsageTelemetryHook();
4193
4229
  case 'build-stop-hook': return this.getBuildStopHook();
@@ -6711,6 +6747,175 @@ process.stdin.on('end', async () => {
6711
6747
  process.exit(0);
6712
6748
  }
6713
6749
  });
6750
+ `;
6751
+ }
6752
+ getStopGateRouterHook() {
6753
+ const port = this.config.port;
6754
+ return `#!/usr/bin/env node
6755
+ // Unjustified Stop Gate router.
6756
+ //
6757
+ // Thin client: reads Stop-hook JSON from stdin, asks the local Instar server
6758
+ // for hot-path state, and in shadow/enforce mode submits trusted evidence
6759
+ // metadata to /internal/stop-gate/evaluate. Shadow mode only records telemetry;
6760
+ // enforce mode blocks only on a server-side "continue" decision.
6761
+
6762
+ const _r = require;
6763
+ const fs = _r('fs');
6764
+ const path = _r('path');
6765
+ const childProcess = _r('child_process');
6766
+
6767
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
6768
+ const configPath = path.join(projectDir, '.instar', 'config.json');
6769
+ let serverPort = ${port};
6770
+ let authToken = '';
6771
+ try {
6772
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
6773
+ serverPort = cfg.port || ${port};
6774
+ authToken = cfg.authToken || '';
6775
+ } catch {}
6776
+
6777
+ function postJson(urlPath, payload, timeoutMs) {
6778
+ const controller = new AbortController();
6779
+ const timer = setTimeout(function () { controller.abort(); }, timeoutMs);
6780
+ return fetch('http://127.0.0.1:' + serverPort + urlPath, {
6781
+ method: 'POST',
6782
+ headers: {
6783
+ 'Content-Type': 'application/json',
6784
+ 'Authorization': 'Bearer ' + authToken,
6785
+ },
6786
+ body: JSON.stringify(payload),
6787
+ signal: controller.signal,
6788
+ }).then(async function (res) {
6789
+ clearTimeout(timer);
6790
+ if (!res.ok) throw new Error('http ' + res.status);
6791
+ return res.json();
6792
+ }, function (err) {
6793
+ clearTimeout(timer);
6794
+ throw err;
6795
+ });
6796
+ }
6797
+
6798
+ function getJson(urlPath, timeoutMs) {
6799
+ const controller = new AbortController();
6800
+ const timer = setTimeout(function () { controller.abort(); }, timeoutMs);
6801
+ return fetch('http://127.0.0.1:' + serverPort + urlPath, {
6802
+ headers: { 'Authorization': 'Bearer ' + authToken },
6803
+ signal: controller.signal,
6804
+ }).then(async function (res) {
6805
+ clearTimeout(timer);
6806
+ if (!res.ok) throw new Error('http ' + res.status);
6807
+ return res.json();
6808
+ }, function (err) {
6809
+ clearTimeout(timer);
6810
+ throw err;
6811
+ });
6812
+ }
6813
+
6814
+ function git(args) {
6815
+ try {
6816
+ return childProcess.execFileSync('git', ['-C', projectDir].concat(args), {
6817
+ encoding: 'utf-8',
6818
+ timeout: 800,
6819
+ stdio: ['ignore', 'pipe', 'ignore'],
6820
+ }).trim();
6821
+ } catch {
6822
+ return '';
6823
+ }
6824
+ }
6825
+
6826
+ function firstLine(value) {
6827
+ return String(value || '').split(/\\r?\\n/).filter(Boolean)[0] || null;
6828
+ }
6829
+
6830
+ function listEvidenceArtifacts(sessionStartTs) {
6831
+ const out = git(['ls-files']);
6832
+ if (!out) return [];
6833
+ const files = out.split(/\\r?\\n/).filter(function (file) {
6834
+ if (!file) return false;
6835
+ if (!/\\.(md|markdown|json|jsonl|txt)$/i.test(file)) return false;
6836
+ return /(^|\\/)(docs\\/specs|specs|plans?|tasks?|upgrades|MEMORY\\.md|AGENTS\\.md)|spec|plan|handoff|todo|next|round/i.test(file);
6837
+ }).slice(0, 30);
6838
+ return files.map(function (file) {
6839
+ const introducingCommit = firstLine(git(['log', '--follow', '--format=%H', '--reverse', '--', file]));
6840
+ const latestCommit = firstLine(git(['log', '--format=%H', '-1', '--', file]));
6841
+ let createdThisSession = false;
6842
+ let modifiedThisSession = false;
6843
+ if (sessionStartTs && latestCommit) {
6844
+ const ts = Number(firstLine(git(['show', '-s', '--format=%ct', latestCommit])) || '0') * 1000;
6845
+ modifiedThisSession = ts >= sessionStartTs;
6846
+ if (introducingCommit) {
6847
+ const createdTs = Number(firstLine(git(['show', '-s', '--format=%ct', introducingCommit])) || '0') * 1000;
6848
+ createdThisSession = createdTs >= sessionStartTs;
6849
+ }
6850
+ }
6851
+ return {
6852
+ path: file,
6853
+ introducingCommit: introducingCommit,
6854
+ latestCommit: latestCommit,
6855
+ createdThisSession: createdThisSession,
6856
+ modifiedThisSession: modifiedThisSession,
6857
+ };
6858
+ });
6859
+ }
6860
+
6861
+ function buildSignals(stopReason, message) {
6862
+ const text = String(stopReason || '') + '\\n' + String(message || '');
6863
+ return {
6864
+ mentionsContextLimit: /context|window|token|compact/i.test(text),
6865
+ mentionsFreshSession: /fresh session|new session|restart|continue in a new/i.test(text),
6866
+ claimsShouldStopForContext: /stop|pause|wrap up|hand off/i.test(text) && /context|fresh|compact/i.test(text),
6867
+ };
6868
+ }
6869
+
6870
+ function exitOpen() {
6871
+ process.exit(0);
6872
+ }
6873
+
6874
+ let data = '';
6875
+ process.stdin.on('data', function (chunk) { data += chunk; });
6876
+ process.stdin.on('end', async function () {
6877
+ let input;
6878
+ try {
6879
+ input = data ? JSON.parse(data) : {};
6880
+ } catch {
6881
+ exitOpen();
6882
+ return;
6883
+ }
6884
+
6885
+ if (input.stop_hook_active) exitOpen();
6886
+ const sessionId = String(input.session_id || input.sessionId || process.env.INSTAR_SESSION_ID || 'unknown');
6887
+
6888
+ try {
6889
+ const hot = await getJson('/internal/stop-gate/hot-path?session=' + encodeURIComponent(sessionId), 1500);
6890
+ if (!hot || hot.killSwitch || hot.mode === 'off' || hot.compactionInFlight) exitOpen();
6891
+
6892
+ const message = String(input.last_assistant_message || '');
6893
+ const stopReason = String(input.stop_reason || input.reason || message || '');
6894
+ const evidenceMetadata = {
6895
+ artifacts: listEvidenceArtifacts(hot.sessionStartTs || null),
6896
+ signals: buildSignals(stopReason, message),
6897
+ sessionStartTs: hot.sessionStartTs || null,
6898
+ };
6899
+
6900
+ const result = await postJson('/internal/stop-gate/evaluate', {
6901
+ sessionId: sessionId,
6902
+ evidenceMetadata: evidenceMetadata,
6903
+ untrustedContent: {
6904
+ stopReason: stopReason,
6905
+ recentTurns: message ? [{ source: 'agent', text: message }] : [],
6906
+ },
6907
+ }, 2500);
6908
+
6909
+ if (hot.mode === 'enforce' && result && result.decision === 'continue' && result.reminder) {
6910
+ process.stdout.write(JSON.stringify({ decision: 'block', reason: result.reminder }));
6911
+ process.exit(2);
6912
+ return;
6913
+ }
6914
+ exitOpen();
6915
+ } catch {
6916
+ exitOpen();
6917
+ }
6918
+ });
6714
6919
  `;
6715
6920
  }
6716
6921
  getClaimInterceptResponseHook() {
@@ -6942,6 +7147,68 @@ if [ "\$PHASE" = "complete" ] || [ "\$PHASE" = "failed" ] || [ "\$PHASE" = "esca
6942
7147
  exit 0
6943
7148
  fi
6944
7149
 
7150
+ # ── Session-scope ownership (BUILD-STOP-HOOK-SESSION-SCOPING-SPEC) ───────────
7151
+ # build-state stamps the owning session (tmux name + Claude session UUID) at /build
7152
+ # start. Only the OWNER session's Stop should be blocked; any other concurrent
7153
+ # session of the same agent must approve-exit WITHOUT spending the owner's
7154
+ # reinforcement budget. This closes the cross-session stop-hook leak + budget drain.
7155
+ HOOK_INPUT=\$(cat 2>/dev/null || echo "")
7156
+ HOOK_SESSION=\$(printf '%s' "\$HOOK_INPUT" | python3 -c "import sys,json
7157
+ try: print((json.load(sys.stdin) or {}).get('session_id','') or '')
7158
+ except Exception: print('')" 2>/dev/null)
7159
+
7160
+ # Resolve MY tmux session name (the stable, cwd-independent owner address).
7161
+ # Test seams: INSTAR_HOOK_TMUX_SESSION (if set, even empty, wins);
7162
+ # INSTAR_HOOK_NO_TMUX=1 forces empty.
7163
+ if [ "\${INSTAR_HOOK_NO_TMUX:-}" = "1" ]; then
7164
+ MY_TMUX=""
7165
+ elif [ -n "\${INSTAR_HOOK_TMUX_SESSION+x}" ]; then
7166
+ MY_TMUX="\${INSTAR_HOOK_TMUX_SESSION}"
7167
+ else
7168
+ MY_TMUX=\$(tmux display-message -p '#S' 2>/dev/null || echo "")
7169
+ fi
7170
+
7171
+ OWNERSHIP=\$(STATE_FILE="\$STATE_FILE" MY_TMUX="\$MY_TMUX" HOOK_SESSION="\$HOOK_SESSION" python3 -c "
7172
+ import json, os, sys
7173
+ try:
7174
+ state = json.load(open(os.environ['STATE_FILE']))
7175
+ except Exception:
7176
+ print('approve'); sys.exit(0)
7177
+ owner = state.get('owner') or {}
7178
+ o_tmux = owner.get('tmux') or ''
7179
+ o_sess = owner.get('session') or ''
7180
+ my_tmux = os.environ.get('MY_TMUX', '')
7181
+ my_sess = os.environ.get('HOOK_SESSION', '')
7182
+
7183
+ # (a) No owner stamped -> conservative no-adopt: approve, never claim ownership.
7184
+ if not o_tmux and not o_sess:
7185
+ print('approve'); sys.exit(0)
7186
+
7187
+ # (b)/(c) Owner stamped: block only the proven owner. A session that cannot match
7188
+ # (including one with no resolvable identity) is approved -> never trap, no drain.
7189
+ is_owner = (bool(o_tmux) and o_tmux == my_tmux) or (bool(o_sess) and o_sess == my_sess)
7190
+ if not is_owner:
7191
+ print('approve'); sys.exit(0)
7192
+
7193
+ # Owner confirmed. Restart reconcile: ONLY on a confirmed tmux-owner match whose
7194
+ # session UUID rotated (restart) do we update owner.session. The write is gated
7195
+ # strictly behind the tmux match, so a non-owner can never clobber owner.session.
7196
+ if o_tmux and o_tmux == my_tmux and my_sess and o_sess != my_sess:
7197
+ owner['session'] = my_sess
7198
+ state['owner'] = owner
7199
+ try:
7200
+ with open(os.environ['STATE_FILE'], 'w') as f:
7201
+ json.dump(state, f, indent=2)
7202
+ except Exception:
7203
+ pass
7204
+ print('owner')
7205
+ " 2>/dev/null)
7206
+
7207
+ if [ "\$OWNERSHIP" != "owner" ]; then
7208
+ echo '{"decision":"approve"}'
7209
+ exit 0
7210
+ fi
7211
+
6945
7212
  # Check and update reinforcement counter
6946
7213
  RESULT=\$(python3 -c "
6947
7214
  import json, sys