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.
- package/dist/commands/init.js +14 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +86 -4
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +2 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +279 -12
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/installCodexHooks.d.ts.map +1 -1
- package/dist/core/installCodexHooks.js +3 -2
- package/dist/core/installCodexHooks.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +1 -8
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +28 -67
- package/dist/server/routes.js.map +1 -1
- package/dist/server/stopGate.d.ts +8 -2
- package/dist/server/stopGate.d.ts.map +1 -1
- package/dist/server/stopGate.js +42 -2
- package/dist/server/stopGate.js.map +1 -1
- package/dist/threadline/CollaborationSurfacer.d.ts.map +1 -1
- package/dist/threadline/CollaborationSurfacer.js +7 -3
- package/dist/threadline/CollaborationSurfacer.js.map +1 -1
- package/dist/threadline/hubCommands.d.ts +67 -0
- package/dist/threadline/hubCommands.d.ts.map +1 -0
- package/dist/threadline/hubCommands.js +126 -0
- package/dist/threadline/hubCommands.js.map +1 -0
- package/package.json +1 -1
- package/playbook-scripts/build-state.py +39 -1
- package/scripts/analyze-release.js +16 -8
- package/scripts/generate-builtin-manifest.cjs +2 -1
- package/src/data/builtin-manifest.json +74 -65
- package/src/scaffold/templates.ts +1 -8
- package/src/templates/hooks/build-stop-hook.sh +62 -0
- package/src/templates/hooks/settings-template.json +10 -0
- package/upgrades/1.3.0.md +27 -0
- package/upgrades/1.3.1.md +27 -0
- package/upgrades/side-effects/build-stop-hook-session-scoping.md +133 -0
- package/upgrades/side-effects/fresh-session-stop-gate-shadow-wiring.md +35 -0
- 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;
|
|
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
|
-
//
|
|
2116
|
-
|
|
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>"**,
|
|
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"
|
|
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
|