mirror-mind 0.34.0 → 0.35.0
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/.pi/skills/mm-build/SKILL.md +1 -1
- package/.pi/skills/mm-explore/SKILL.md +1 -1
- package/.pi/skills/mm-help/SKILL.md +2 -2
- package/.pi/skills/mm-soul/SKILL.md +1 -1
- package/AGENTS.md +7 -6
- package/README.md +5 -6
- package/docs/releases/index.md +1 -0
- package/docs/releases/v0.35.0.md +199 -0
- package/package.json +2 -4
- package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
- package/plugins/mirror-mind/agy/pre-invocation.sh +92 -0
- package/plugins/mirror-mind/agy/session-start.sh +92 -0
- package/plugins/mirror-mind/agy/stop.sh +92 -0
- package/plugins/mirror-mind/hooks.json +26 -0
- package/plugins/mirror-mind/mcp_config.json +11 -0
- package/plugins/mirror-mind/plugin.json +4 -0
- package/plugins/mirror-mind/rules/AGENTS.md +189 -0
- package/plugins/mirror-mind/skills/mm-build/SKILL.md +1 -1
- package/ts/src/conversation/agyTranscript.ts +105 -0
- package/ts/src/conversation/commandTurn.ts +1 -1
- package/ts/src/conversation/deferredCloseTail.ts +2 -2
- package/ts/src/conversation/logger.ts +1 -1
- package/ts/src/conversation/runtimePolicy.ts +10 -1
- package/ts/src/docs/docsLint.ts +14 -1
- package/ts/src/frontDoor/stdinSource.ts +2 -2
- package/ts/src/guards/claudePlugin.ts +74 -0
- package/ts/src/guards/packContents.ts +3 -2
- package/ts/src/guards/piSourcedSkills.ts +1 -1
- package/ts/src/guards/retiredSurfaces.ts +31 -0
- package/ts/src/hooks/agy.ts +229 -0
- package/ts/src/hooks/main.ts +11 -15
- package/ts/src/hooks/payload.ts +8 -15
- package/ts/src/hooks/runtime.ts +11 -10
- package/ts/src/runtime/wiring.ts +18 -36
- package/.gemini/hooks/log-assistant.sh +0 -85
- package/.gemini/hooks/log-user.sh +0 -85
- package/.gemini/hooks/session-end.sh +0 -85
- package/.gemini/hooks/session-start.sh +0 -85
- package/ts/src/hooks/gemini.ts +0 -128
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"mirror-mind": {
|
|
4
|
+
"command": "/bin/sh",
|
|
5
|
+
"args": [
|
|
6
|
+
"-c",
|
|
7
|
+
"mmbin=\"\"; [ -n \"$MIRROR_BIN\" ] && [ -x \"$MIRROR_BIN/mirror\" ] && mmbin=\"$MIRROR_BIN/mirror\"; [ -n \"$mmbin\" ] || mmbin=\"$(command -v mirror 2>/dev/null)\"; for mmdir in \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmbin\" ] && break; [ -x \"$mmdir/mirror\" ] && mmbin=\"$mmdir/mirror\"; done; [ -n \"$mmbin\" ] || { echo \"mirror-mind: mirror not found on PATH; the MCP server cannot start. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory.\" >&2; exit 1; }; mmnode=\"$MIRROR_NODE\"; [ -n \"$mmnode\" ] && [ -x \"$mmnode\" ] || mmnode=\"$(command -v node 2>/dev/null)\"; for mmdir in \"$(dirname \"$mmbin\")\" \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmnode\" ] && break; [ -x \"$mmdir/node\" ] && mmnode=\"$mmdir/node\"; done; [ -n \"$mmnode\" ] || { echo \"mirror-mind: node not found on PATH; the MCP server cannot start. Set MIRROR_NODE.\" >&2; exit 1; }; exec \"$mmnode\" \"$mmbin\" mcp"
|
|
8
|
+
]
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Mirror Mind — Operating Instructions
|
|
2
|
+
|
|
3
|
+
<!-- This file ships in the mirror-mind package and is read by every runtime:
|
|
4
|
+
Pi and Codex read it from the project root natively, Claude Code through
|
|
5
|
+
the @AGENTS.md import in CLAUDE.md, Antigravity as the plugin's rules, and
|
|
6
|
+
an installed package's Pi session receives it from the extension
|
|
7
|
+
(CV22.DS10.US3, D13).
|
|
8
|
+
It holds nothing about this repository; that is CLAUDE.md's part. -->
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Mirror Operating Instructions
|
|
13
|
+
|
|
14
|
+
Applies to all sessions, regardless of project.
|
|
15
|
+
|
|
16
|
+
### Operating Modes
|
|
17
|
+
|
|
18
|
+
The mirror operates in four modes, chosen automatically based on context:
|
|
19
|
+
Mirror, Builder, Explorer, and Soul.
|
|
20
|
+
|
|
21
|
+
Each skill below is named three ways: `/mm-<skill>` in Pi, `$mm-<skill>` in
|
|
22
|
+
Codex, `/mm:<skill>` in Claude Code. A **plugin** install prefixes the plugin's
|
|
23
|
+
name — `/mirror-mind:mm:<skill>` in Claude Code's menu and in Antigravity,
|
|
24
|
+
`$mirror-mind:mm:<skill>` in Codex, `mirror-mind:mm-<skill>` as the skill's own
|
|
25
|
+
name — and the forms below refer to the same skill.
|
|
26
|
+
|
|
27
|
+
**Mirror Mode** — activate for: life decisions, feelings, business strategy,
|
|
28
|
+
writing, mentoring, health, existential questions, sensemaking, psychological
|
|
29
|
+
tensions, class preparation, product launches, or any topic asking for personal
|
|
30
|
+
reflection or positioning.
|
|
31
|
+
|
|
32
|
+
How to operate: load the mirror skill (`/mm-mirror`, `$mm-mirror`, or
|
|
33
|
+
`/mm:mirror`). Load identity, route persona, search attachments, answer in first
|
|
34
|
+
person, and record the response.
|
|
35
|
+
|
|
36
|
+
**Builder Mode** — activate for: code, project structure, YAML editing, bugs,
|
|
37
|
+
implementation, architecture, or any software engineering task.
|
|
38
|
+
|
|
39
|
+
How to operate: read code, edit files, run commands, propose technical
|
|
40
|
+
solutions, keep docs updated when code changes. For a journey, use `/mm-build
|
|
41
|
+
<slug>` / `$mm-build <slug>` / `/mm:build <slug>` — loads journey context and
|
|
42
|
+
project docs.
|
|
43
|
+
|
|
44
|
+
**Explorer Mode** — activate for the uncertain middle before construction:
|
|
45
|
+
exploring a possibility space, holding uncertainty before committing to build,
|
|
46
|
+
developing an Exploratory Story, surfacing attractors, proposing small
|
|
47
|
+
experiments, or preparing a Builder handoff.
|
|
48
|
+
|
|
49
|
+
How to operate: use `/mm-explore <slug>` / `$mm-explore <slug>` / `/mm:explore
|
|
50
|
+
<slug>` to activate Explorer Mode for a journey and resume the active Exploratory
|
|
51
|
+
Story when one exists. Follow `.pi/skills/mm-explore/SKILL.md`: render required
|
|
52
|
+
story surfaces before interpretation, thicken the story as the narrative changes,
|
|
53
|
+
and keep exploration durable. Explorer Mode preserves uncertainty — it does not
|
|
54
|
+
implement. For operational mutation requests, name the `△ EXPLORER → BUILDER
|
|
55
|
+
BOUNDARY` and route them through Builder Mode via an explicit handoff. Deactivate
|
|
56
|
+
with `/mm-explore deactivate`, which returns to Mirror Mode while preserving
|
|
57
|
+
sticky journey context.
|
|
58
|
+
|
|
59
|
+
**Soul Mode** — activate when the user asks to enter Soul Mode, open Soul Mode,
|
|
60
|
+
or continue an active Soul Mode ritual.
|
|
61
|
+
|
|
62
|
+
How to operate: use `/mm-soul [slug]` / `$mm-soul [slug]` / `/mm:soul [slug]`
|
|
63
|
+
to render the entry surface. While Soul Mode is active, follow
|
|
64
|
+
`.pi/skills/mm-soul/SKILL.md`: listen to the user's answer as living field,
|
|
65
|
+
withhold Possible Listenings when the material is thin, and when living matter
|
|
66
|
+
appears call the front door's `soul listen ...` (the exact invocation is in the
|
|
67
|
+
skill) to render the Possible Listenings surface at the end of the response. This renderer call is required
|
|
68
|
+
Soul Mode behavior, not optional tool use. Soul Mode must not mutate files,
|
|
69
|
+
implement stories, run implementation commands, package releases, or change
|
|
70
|
+
project state. For operational requests, name the `☾ SOUL → BUILDER BOUNDARY`
|
|
71
|
+
and ask whether to switch to Builder Mode for the same journey.
|
|
72
|
+
|
|
73
|
+
Builder activation boundary: activating Builder Mode or loading a journey is
|
|
74
|
+
context setup only. After loading the context and required docs, stop and ask
|
|
75
|
+
what work should be done next. Do not edit files, create tests, run
|
|
76
|
+
implementation, start TDD, or mutate project state until the user gives an
|
|
77
|
+
explicit implementation or documentation instruction, such as implement, fix,
|
|
78
|
+
edit, create, run tests, or names a specific story to execute. Context
|
|
79
|
+
activation is not execution consent.
|
|
80
|
+
|
|
81
|
+
Ariad surface transport invariant: when command output contains marked Ariad
|
|
82
|
+
surface blocks (`<<<ARIAD:...>>>` through `<<<END:...>>>`), preserve every marked
|
|
83
|
+
block verbatim in the assistant response before commentary. Do not summarize,
|
|
84
|
+
translate, reorder, trim, rewrap, re-indent, reformat, or replace marked surfaces
|
|
85
|
+
with prose. This is a general rendering rule for Ariad surfaces, not a
|
|
86
|
+
phase-specific behavior.
|
|
87
|
+
|
|
88
|
+
**Ambiguity:** if the mode is unclear, ask whether the user wants personal
|
|
89
|
+
reflection or project construction.
|
|
90
|
+
|
|
91
|
+
**Journey Status** — shortcut within Mirror Mode. Activate when the user asks
|
|
92
|
+
"How are we doing?", "What's the status of X?", or any question about progress
|
|
93
|
+
or roadmap. Dispatch to `/mm-journey` or `/mm-journeys`. When the user asks for
|
|
94
|
+
the journey list in natural language, use `/mm-journeys` and preserve its
|
|
95
|
+
hierarchical rendering without flattening or reformatting it.
|
|
96
|
+
|
|
97
|
+
**Commits:** use descriptive English commit messages. Explain the WHY, not just
|
|
98
|
+
what was done. Prefer small commits with clear review boundaries.
|
|
99
|
+
|
|
100
|
+
### Ego-Persona Model
|
|
101
|
+
|
|
102
|
+
The mirror has one voice: the ego. Personas are specialized lenses activated by
|
|
103
|
+
the ego according to context.
|
|
104
|
+
|
|
105
|
+
**Automatic routing:** activate a persona when the topic clearly belongs to a
|
|
106
|
+
specialized domain, the depth required exceeds the generic ego repertoire, or
|
|
107
|
+
the user explicitly asks for a persona.
|
|
108
|
+
|
|
109
|
+
**Routing protocol:** persona routing is data-driven. Each persona in the
|
|
110
|
+
database carries `routing_keywords` and a routing descriptor. At runtime,
|
|
111
|
+
`detectPersona` (`ts/src/persona/detectPersona.ts`) scores the query against
|
|
112
|
+
those keywords. If no persona scores above threshold, the ego answers alone. To
|
|
113
|
+
inspect active routing:
|
|
114
|
+
`mirror detect-persona "<query>"`.
|
|
115
|
+
|
|
116
|
+
**Signature format:**
|
|
117
|
+
|
|
118
|
+
When the ego answers alone — no signature.
|
|
119
|
+
|
|
120
|
+
When a persona is active:
|
|
121
|
+
```text
|
|
122
|
+
◇ persona-name
|
|
123
|
+
|
|
124
|
+
[first-person answer, unified voice]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
When switching personas:
|
|
128
|
+
```text
|
|
129
|
+
◇ product-designer
|
|
130
|
+
|
|
131
|
+
[analysis...]
|
|
132
|
+
|
|
133
|
+
◇ therapist
|
|
134
|
+
|
|
135
|
+
[reflection...]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Rules: `◇` plus persona name on its own line; voice stays first person and unified.
|
|
139
|
+
|
|
140
|
+
### Hard Constraints
|
|
141
|
+
|
|
142
|
+
- **Truth:** do not invent data. If uncertain, say so.
|
|
143
|
+
- **Service:** intellectual partner, not task executor. Question, refine, align —
|
|
144
|
+
do not execute without thinking.
|
|
145
|
+
|
|
146
|
+
### Available Skills
|
|
147
|
+
|
|
148
|
+
**Core modes:**
|
|
149
|
+
- `mm-mirror` — activates Mirror Mode — `.pi/skills/mm-mirror/SKILL.md`
|
|
150
|
+
- `mm-build` — activates Builder Mode for a journey — `.pi/skills/mm-build/SKILL.md`
|
|
151
|
+
- `mm-explore` — activates Explorer Mode for a journey — `.pi/skills/mm-explore/SKILL.md`
|
|
152
|
+
- `mm-soul` — activates Soul Mode ritual entry — `.pi/skills/mm-soul/SKILL.md`
|
|
153
|
+
|
|
154
|
+
**Journeys and tasks:**
|
|
155
|
+
- `mm-journeys` — compact journey list — `.pi/skills/mm-journeys/SKILL.md`
|
|
156
|
+
- `mm-journey` — detailed journey status — `.pi/skills/mm-journey/SKILL.md`
|
|
157
|
+
- `mm-tasks` — task management by journey — `.pi/skills/mm-tasks/SKILL.md`
|
|
158
|
+
- `mm-week` — weekly planning — `.pi/skills/mm-week/SKILL.md`
|
|
159
|
+
|
|
160
|
+
**Memory and inspection:**
|
|
161
|
+
- `mm-memories` — recorded memories — `.pi/skills/mm-memories/SKILL.md`
|
|
162
|
+
- `mm-conversations` — recent conversations list — `.pi/skills/mm-conversations/SKILL.md`
|
|
163
|
+
- `mm-recall` — load messages from a previous conversation — `.pi/skills/mm-recall/SKILL.md`
|
|
164
|
+
|
|
165
|
+
**Content:**
|
|
166
|
+
- `mm-journal` — personal journal entry — `.pi/skills/mm-journal/SKILL.md`
|
|
167
|
+
|
|
168
|
+
**Identity:**
|
|
169
|
+
- `mm-seed` — seed identity YAML files into the database — `.pi/skills/mm-seed/SKILL.md`
|
|
170
|
+
- `mm-identity` — read and update identity in the database — `.pi/skills/mm-identity/SKILL.md`
|
|
171
|
+
|
|
172
|
+
**Session control:**
|
|
173
|
+
- `mm-mute` — toggle conversation logging — `.pi/skills/mm-mute/SKILL.md`
|
|
174
|
+
- `mm-new` — start a new conversation — `.pi/skills/mm-new/SKILL.md`
|
|
175
|
+
- `mm-discard` — discard the current conversation before quitting — `.pi/skills/mm-discard/SKILL.md`
|
|
176
|
+
|
|
177
|
+
**Memory cultivation:**
|
|
178
|
+
- `mm-consolidate` — scan memories for patterns and propose consolidation — `.pi/skills/mm-consolidate/SKILL.md`
|
|
179
|
+
- `mm-shadow` — surface and promote shadow-layer observations — `.pi/skills/mm-shadow/SKILL.md`
|
|
180
|
+
|
|
181
|
+
**Utilities:**
|
|
182
|
+
- `mm-consult` — ask other LLMs through OpenRouter — `.pi/skills/mm-consult/SKILL.md`
|
|
183
|
+
- `mm-backup` — memory database backup — `.pi/skills/mm-backup/SKILL.md`
|
|
184
|
+
- `mm-welcome` — render the state-aware welcome card on demand — `.pi/skills/mm-welcome/SKILL.md`
|
|
185
|
+
- `mm-release-notes` — show Mirror Mind release notes — `.pi/skills/mm-release-notes/SKILL.md`
|
|
186
|
+
- `mm-update` — update the local Mirror runtime — `.pi/skills/mm-update/SKILL.md`
|
|
187
|
+
- `mm-help` — list available commands — `.pi/skills/mm-help/SKILL.md`
|
|
188
|
+
|
|
189
|
+
Full command reference: [REFERENCE.md](REFERENCE.md)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The agy transcript reader (CV21.DS7.TS3).
|
|
2
|
+
//
|
|
3
|
+
// Antigravity's hook payloads carry no prompt or reply text (DS12.TS3 L7),
|
|
4
|
+
// so logging reads the JSONL transcript agy writes. The step shapes are
|
|
5
|
+
// probed from real agy 1.4.2 transcripts (the US1/TS2/TS3 probe discipline;
|
|
6
|
+
// evidence/transcript-shape-20261018.md):
|
|
7
|
+
//
|
|
8
|
+
// * a user turn is a `USER_INPUT` step (`source: USER_EXPLICIT`) whose
|
|
9
|
+
// content string wraps the request in `<USER_REQUEST>…</USER_REQUEST>`,
|
|
10
|
+
// followed by `<ADDITIONAL_METADATA>…`;
|
|
11
|
+
// * the model's answer is the `PLANNER_RESPONSE` step(s) (`source: MODEL`).
|
|
12
|
+
// MOST of these are EMPTY — intermediate tool steps — so the reply is
|
|
13
|
+
// the non-empty response text in the turn, not every PLANNER_RESPONSE.
|
|
14
|
+
// (The plan guessed `AGENT_MESSAGE`; the probe corrected both the type
|
|
15
|
+
// and the emptiness.)
|
|
16
|
+
//
|
|
17
|
+
// A turn runs from one `USER_INPUT` to just before the next. `step_index` is
|
|
18
|
+
// a monotonic per-conversation ordinal; `lastStepIndex` is the turn's
|
|
19
|
+
// high-water mark, which the Stop handler stores so a re-fired Stop logs
|
|
20
|
+
// nothing new.
|
|
21
|
+
|
|
22
|
+
import { parseJsonl } from "#conversation/transcriptBackfill.ts";
|
|
23
|
+
|
|
24
|
+
export interface AgyTurn {
|
|
25
|
+
readonly userText: string;
|
|
26
|
+
readonly replyText: string;
|
|
27
|
+
/** The highest step_index this turn logged: the idempotency high-water mark. */
|
|
28
|
+
readonly lastStepIndex: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stringField(entry: unknown, key: string): string {
|
|
32
|
+
if (typeof entry !== "object" || entry === null) return "";
|
|
33
|
+
const value = (entry as Record<string, unknown>)[key];
|
|
34
|
+
return typeof value === "string" ? value : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function numberField(entry: unknown, key: string): number {
|
|
38
|
+
if (typeof entry !== "object" || entry === null) return -1;
|
|
39
|
+
const value = (entry as Record<string, unknown>)[key];
|
|
40
|
+
return typeof value === "number" ? value : -1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const USER_REQUEST = /<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>/;
|
|
44
|
+
|
|
45
|
+
/** The request body agy wraps the user's words in, or the whole string. */
|
|
46
|
+
export function extractUserRequest(content: string): string {
|
|
47
|
+
const match = USER_REQUEST.exec(content);
|
|
48
|
+
return (match ? match[1] : content).trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Join the turn's non-empty reply parts into one string, the same rule the
|
|
53
|
+
* Claude reader's `assistantText` uses (trimmed, blank-line separated).
|
|
54
|
+
* Shared so "how parts become one reply" lives in one place.
|
|
55
|
+
*/
|
|
56
|
+
export function joinReplyParts(parts: readonly string[]): string {
|
|
57
|
+
return parts
|
|
58
|
+
.map((part) => part.trim())
|
|
59
|
+
.filter((part) => part.length > 0)
|
|
60
|
+
.join("\n\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The transcript's completed turns, in order. Each pairs the user's request
|
|
65
|
+
* with the model's reply and carries the turn's high-water `step_index`.
|
|
66
|
+
* A turn with no reply text yet (the model has not answered) is omitted:
|
|
67
|
+
* the Stop handler logs it on the Stop that follows the answer.
|
|
68
|
+
*/
|
|
69
|
+
export function readAgyTurns(transcriptPath: string): AgyTurn[] {
|
|
70
|
+
let entries: unknown[];
|
|
71
|
+
try {
|
|
72
|
+
entries = parseJsonl(transcriptPath);
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const turns: AgyTurn[] = [];
|
|
78
|
+
let userText: string | null = null;
|
|
79
|
+
let replyParts: string[] = [];
|
|
80
|
+
let lastStepIndex = -1;
|
|
81
|
+
|
|
82
|
+
const flush = () => {
|
|
83
|
+
if (userText === null) return;
|
|
84
|
+
const replyText = joinReplyParts(replyParts);
|
|
85
|
+
if (replyText) turns.push({ userText, replyText, lastStepIndex });
|
|
86
|
+
userText = null;
|
|
87
|
+
replyParts = [];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const type = stringField(entry, "type");
|
|
92
|
+
if (type === "USER_INPUT") {
|
|
93
|
+
flush();
|
|
94
|
+
userText = extractUserRequest(stringField(entry, "content"));
|
|
95
|
+
lastStepIndex = numberField(entry, "step_index");
|
|
96
|
+
} else if (type === "PLANNER_RESPONSE" && userText !== null) {
|
|
97
|
+
const content = stringField(entry, "content").trim();
|
|
98
|
+
const stepIndex = numberField(entry, "step_index");
|
|
99
|
+
if (stepIndex > lastStepIndex) lastStepIndex = stepIndex;
|
|
100
|
+
if (content) replyParts.push(content);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
flush();
|
|
104
|
+
return turns;
|
|
105
|
+
}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// One rule for every runtime: a Claude Code user who types `$mm-journeys`,
|
|
30
30
|
// which Claude Code does not understand, is still invoking a skill, and that
|
|
31
31
|
// turn is not conversation either. The rule has one home for every hook that
|
|
32
|
-
// runs in our process (the Claude-family logger,
|
|
32
|
+
// runs in our process (the Claude-family logger, agy's Stop); the Pi
|
|
33
33
|
// extension, a separate process in Pi's file, keeps its own `/` check.
|
|
34
34
|
|
|
35
35
|
const SKILL_SEGMENT = "[a-z][a-z0-9_-]*";
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// (extraction, then title/summary/tags). Some runtimes give a SessionEnd hook
|
|
6
6
|
// less time than one model call: Codex runs it synchronously under a 1 s
|
|
7
7
|
// default (DS12.TS2 L2; DS2's live `/hooks` table), Grok under 1.5 s
|
|
8
|
-
// (DS12.TS4). Pi
|
|
9
|
-
//
|
|
8
|
+
// (DS12.TS4). Pi already closes without extraction at exit and
|
|
9
|
+
// lets the next session start extract (`session-end-pi`). On a runtime that
|
|
10
10
|
// bounds the hook, the shared Claude-family `session-end` does the same, and a
|
|
11
11
|
// little more: it also skips the metadata finalization, and flags the
|
|
12
12
|
// conversation so the next start's maintenance finishes it.
|
|
@@ -417,7 +417,7 @@ export async function switchConversation(
|
|
|
417
417
|
* Python `end_session`: close the bound conversation and deactivate.
|
|
418
418
|
*
|
|
419
419
|
* The session is deactivated BEFORE the tail, not after (CV21.DS4.TS2). A tail
|
|
420
|
-
* cut by the runtime's bound (Codex 1 s, Grok 1.5 s
|
|
420
|
+
* cut by the runtime's bound (Codex 1 s, Grok 1.5 s) or a
|
|
421
421
|
* background hook that dies used to leave the session `active = 1`: the row
|
|
422
422
|
* `guessRuntimeSessionId` reads as "the session", which is how another
|
|
423
423
|
* runtime's agent wrote into a Codex conversation (DS12.TS2 L9).
|
|
@@ -40,11 +40,20 @@ export function sessionEndRunsLlmTail(interfaceName: string | null | undefined):
|
|
|
40
40
|
* (DS2). The session is still re-measured by the next story that
|
|
41
41
|
* depends on it.
|
|
42
42
|
*
|
|
43
|
+
* antigravity 1.4.2: agy exposes no SessionEnd hook at all (its contracts
|
|
44
|
+
* are SessionStart, PreTool, PostTool, PreInvocation,
|
|
45
|
+
* PostInvocation, Stop; CV21.DS7.TS2 probe). Stop fires at each
|
|
46
|
+
* turn's end, never at session end, so nothing closes the
|
|
47
|
+
* conversation at TUI exit — the same situation as Codex, reached
|
|
48
|
+
* by a different absence. The agy Stop handler logs each turn and
|
|
49
|
+
* leaves the conversation open (CV21.DS7.TS3); the orphan close
|
|
50
|
+
* is what ends it, and the next SessionStart sweeps it.
|
|
51
|
+
*
|
|
43
52
|
* For such a runtime, an open conversation idle past the orphan threshold is an
|
|
44
53
|
* orphan even while its session is active, and the close deactivates the session
|
|
45
54
|
* (`closeStaleOrphans`). Every other runtime's session ends with its TUI, and an
|
|
46
55
|
* active session there means a person who may still be typing.
|
|
47
56
|
*/
|
|
48
57
|
export function sessionOutlivesTui(interfaceName: string | null | undefined): boolean {
|
|
49
|
-
return interfaceName === "codex";
|
|
58
|
+
return interfaceName === "codex" || interfaceName === "antigravity";
|
|
50
59
|
}
|
package/ts/src/docs/docsLint.ts
CHANGED
|
@@ -49,6 +49,14 @@ const FIXTURE_DIR_NAME = "fixtures";
|
|
|
49
49
|
// itself has no `index.md`, by design. Not a broken link.
|
|
50
50
|
const TEMPLATE_DIR_MARKER = "/roadmap/templates/";
|
|
51
51
|
|
|
52
|
+
// Generated byte copies of a root document, each held equal to its source by
|
|
53
|
+
// a guard (`claudePlugin.ts`: the plugin's `rules/AGENTS.md` is `AGENTS.md`'s
|
|
54
|
+
// bytes, CV21.DS7.US1). Their links are the source's links, authored at the
|
|
55
|
+
// repo root, so they resolve there -- a root-relative `REFERENCE.md` can never
|
|
56
|
+
// exist under `rules/`, and skipping the file would hide a link the source
|
|
57
|
+
// itself cannot resolve.
|
|
58
|
+
const ROOT_COPIES = new Set(["plugins/mirror-mind/rules/AGENTS.md"]);
|
|
59
|
+
|
|
52
60
|
export interface BrokenLink {
|
|
53
61
|
readonly sourceFile: string;
|
|
54
62
|
readonly line: number;
|
|
@@ -115,6 +123,11 @@ function isTemplatePlaceholder(sourceFile: string, targetPath: string): boolean
|
|
|
115
123
|
return toPosix(sourceFile).includes(TEMPLATE_DIR_MARKER) && targetPath === "index.md";
|
|
116
124
|
}
|
|
117
125
|
|
|
126
|
+
/** The directory a file's relative links resolve from: its own, or the root for a root copy. */
|
|
127
|
+
function linkBase(sourceFile: string, repoRoot: string): string {
|
|
128
|
+
return ROOT_COPIES.has(toPosix(relative(repoRoot, sourceFile))) ? repoRoot : dirname(sourceFile);
|
|
129
|
+
}
|
|
130
|
+
|
|
118
131
|
/** True for a `fixtures` directory nested under a `test`/`tests` one. */
|
|
119
132
|
function isTestFixturePath(path: string): boolean {
|
|
120
133
|
const parts = toPosix(path).split("/");
|
|
@@ -185,7 +198,7 @@ export function checkFile(sourceFile: string, repoRoot: string): BrokenLink[] {
|
|
|
185
198
|
const anchorPart = hashIndex === -1 ? "" : link.slice(hashIndex + 1);
|
|
186
199
|
if (isTemplatePlaceholder(sourceFile, pathPart)) continue;
|
|
187
200
|
|
|
188
|
-
const target = normalize(join(
|
|
201
|
+
const target = normalize(join(linkBase(sourceFile, repoRoot), pathPart));
|
|
189
202
|
if (!existsSync(target)) {
|
|
190
203
|
problems.push({
|
|
191
204
|
sourceFile: relSource,
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
// A one-shot handoff rather than an argument because the hooks run the front
|
|
11
11
|
// door in-process (`runFrontDoor`), through the same `main(argv)` every other
|
|
12
12
|
// caller uses, and a payload does not belong in argv: argv is what `ps`
|
|
13
|
-
// shows, and what the Gemini hooks
|
|
14
|
-
// this seam avoids repeating.
|
|
13
|
+
// shows, and what the Gemini CLI hooks (retired by CV21.DS8.US1) put a prompt
|
|
14
|
+
// into is the exception this seam avoids repeating.
|
|
15
15
|
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
17
|
|
|
@@ -178,6 +178,71 @@ export function marketplaceJson(): string {
|
|
|
178
178
|
return `${JSON.stringify(buildMarketplace(), null, 2)}\n`;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
// --- the agy-native layer (CV21.DS7.US1) -------------------------------------
|
|
182
|
+
//
|
|
183
|
+
// Antigravity treats a directory as a NATIVE plugin only when a root
|
|
184
|
+
// `plugin.json` marks it (or `.antigravity-plugin/`/`.gemini-plugin/`).
|
|
185
|
+
// Without the marker, `agy plugin install` converts the Claude surfaces as an
|
|
186
|
+
// extension and OVERWRITES a shipped root `hooks.json` with Claude's — the
|
|
187
|
+
// probe on agy 1.4.2 showed both behaviors (evidence in the US1 story
|
|
188
|
+
// package). With the marker, validation passes, our root `hooks.json`
|
|
189
|
+
// survives staging, `mcpServers: 1 processed`, and the 25 skills ride along.
|
|
190
|
+
//
|
|
191
|
+
// The layer is four generated files at agy's documented paths: the marker,
|
|
192
|
+
// the hooks skeleton (TS2 adds injection, TS3 adds logging), the MCP map
|
|
193
|
+
// (DERIVED from the manifest's entry — one locator, every carrier), and
|
|
194
|
+
// `rules/AGENTS.md` (agy merges `rules/` into the active rule set; the guard
|
|
195
|
+
// comparing committed to generated bytes is the sync with `AGENTS.md`).
|
|
196
|
+
|
|
197
|
+
/** The native-plugin marker. Claude Code and Codex read `.claude-plugin/`. */
|
|
198
|
+
export function buildAgyPluginMarker(): Record<string, unknown> {
|
|
199
|
+
return { name: PLUGIN_NAME, description: PLUGIN_DESCRIPTION };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function agyPluginMarkerJson(): string {
|
|
203
|
+
return `${JSON.stringify(buildAgyPluginMarker(), null, 2)}\n`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The named hook with its handlers (CV21.DS7.TS2). Commands are relative:
|
|
208
|
+
* agy sets a handler's cwd to the directory holding `hooks.json`, and the
|
|
209
|
+
* wrappers live beside it in `agy/` (generated agy-form; the pre-invocation
|
|
210
|
+
* wrapper gates on stdin before Node is ever resolved, because
|
|
211
|
+
* `PreInvocation` fires before every model call and blocks the agent loop).
|
|
212
|
+
* Timeouts are explicit and small: the fast path is shell-instant, the slow
|
|
213
|
+
* path is one `mirror load`, and a hang here freezes the agent — which a
|
|
214
|
+
* user reads as "Antigravity broke", not "Mirror hung". `Stop` is TS3's.
|
|
215
|
+
*/
|
|
216
|
+
export function buildAgyHooks(): Record<string, unknown> {
|
|
217
|
+
return {
|
|
218
|
+
[PLUGIN_NAME]: {
|
|
219
|
+
enabled: true,
|
|
220
|
+
SessionStart: [{ type: "command", command: "agy/session-start.sh", timeout: 60 }],
|
|
221
|
+
PreInvocation: [{ type: "command", command: "agy/pre-invocation.sh", timeout: 15 }],
|
|
222
|
+
// CV21.DS7.TS3: Stop fires at each turn's end (agy has no SessionEnd);
|
|
223
|
+
// the handler reads the transcript and logs the turn as antigravity.
|
|
224
|
+
Stop: [{ type: "command", command: "agy/stop.sh", timeout: 30 }],
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function agyHooksJson(): string {
|
|
230
|
+
return `${JSON.stringify(buildAgyHooks(), null, 2)}\n`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** agy's `mcp_config.json`: the same `mcpServers` map the manifest carries. */
|
|
234
|
+
export function buildAgyMcpConfig(): Record<string, unknown> {
|
|
235
|
+
return {
|
|
236
|
+
mcpServers: {
|
|
237
|
+
[PLUGIN_NAME]: { command: MCP_ENTRY.command, args: [...MCP_ENTRY.args] },
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function agyMcpConfigJson(): string {
|
|
243
|
+
return `${JSON.stringify(buildAgyMcpConfig(), null, 2)}\n`;
|
|
244
|
+
}
|
|
245
|
+
|
|
181
246
|
/**
|
|
182
247
|
* The skill markdown file, matching `skill.md` case-insensitively.
|
|
183
248
|
*
|
|
@@ -269,6 +334,15 @@ export function planGeneratedFiles(repoRoot: string): GeneratedFile[] {
|
|
|
269
334
|
content: manifestJson(version),
|
|
270
335
|
},
|
|
271
336
|
{ relativePath: MARKETPLACE_PATH, content: marketplaceJson() },
|
|
337
|
+
// The agy-native layer (CV21.DS7.US1).
|
|
338
|
+
{ relativePath: join(PLUGIN_DIR, "plugin.json"), content: agyPluginMarkerJson() },
|
|
339
|
+
{ relativePath: join(PLUGIN_DIR, "hooks.json"), content: agyHooksJson() },
|
|
340
|
+
{ relativePath: join(PLUGIN_DIR, "mcp_config.json"), content: agyMcpConfigJson() },
|
|
341
|
+
{
|
|
342
|
+
relativePath: join(PLUGIN_DIR, "rules", "AGENTS.md"),
|
|
343
|
+
content: readFileSync(join(repoRoot, "AGENTS.md"), "utf8"),
|
|
344
|
+
source: "AGENTS.md",
|
|
345
|
+
},
|
|
272
346
|
...claudeCopies.values(),
|
|
273
347
|
...[...pluginSkills].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([, file]) => file),
|
|
274
348
|
];
|
|
@@ -26,7 +26,6 @@ export const ALLOWED_PREFIXES: readonly string[] = [
|
|
|
26
26
|
".pi/extensions/",
|
|
27
27
|
".pi/skills/",
|
|
28
28
|
".claude/hooks/",
|
|
29
|
-
".gemini/hooks/",
|
|
30
29
|
"plugins/mirror-mind/",
|
|
31
30
|
];
|
|
32
31
|
|
|
@@ -56,8 +55,10 @@ export const REQUIRED_FILES: readonly string[] = [
|
|
|
56
55
|
".pi/extensions/mirror-logger.ts",
|
|
57
56
|
".pi/skills/mm-mirror/SKILL.md",
|
|
58
57
|
".claude/hooks/session-start.sh",
|
|
59
|
-
".gemini/hooks/session-start.sh",
|
|
60
58
|
"plugins/mirror-mind/hooks/hooks.json",
|
|
59
|
+
// CV21.DS7.US1: the agy-native layer's hooks file. A tarball cannot ship
|
|
60
|
+
// the skills without the layer Antigravity runs them beside.
|
|
61
|
+
"plugins/mirror-mind/hooks.json",
|
|
61
62
|
".agents/plugins/marketplace.json",
|
|
62
63
|
];
|
|
63
64
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Skills with one body in every runtime (CR102).
|
|
2
2
|
//
|
|
3
3
|
// Most Mirror skills have two authored bodies. `.pi/skills/` serves Pi, and
|
|
4
|
-
// through the `.agents/skills/` links Codex
|
|
4
|
+
// through the `.agents/skills/` links Codex too. `.claude/skills/`
|
|
5
5
|
// serves Claude Code, and `claudePlugin.ts` copies it into the plugin. CR071
|
|
6
6
|
// kept the two apart on purpose: Usage sections, example language, and in
|
|
7
7
|
// `mm-soul` and `mm-mirror` whole lines that are true for one runtime only.
|
|
@@ -514,6 +514,37 @@ export const RETIRED: readonly RetiredSurface[] = [
|
|
|
514
514
|
residueScope: ["ts/src/", "ts/scripts/"],
|
|
515
515
|
exemptions: {},
|
|
516
516
|
},
|
|
517
|
+
{
|
|
518
|
+
surfaceId: "gemini-cli",
|
|
519
|
+
story: "CV21.DS8.US1",
|
|
520
|
+
// The Gemini CLI integration: four generated wrappers under `.gemini/hooks`,
|
|
521
|
+
// the `gemini:*` hook family behind them, the two `mirror init` steps, and
|
|
522
|
+
// the Gemini smoke. Retired once DS7.US4 measured Antigravity at L4 on agy
|
|
523
|
+
// 1.4.2 -- a Gemini CLI user loses nothing by moving. `.gemini/` is an
|
|
524
|
+
// absent path and not merely an unallowed one because `mirror init` told
|
|
525
|
+
// users to write absolute paths into that directory into their
|
|
526
|
+
// `~/.gemini/settings.json`: a standing instruction to execute whatever a
|
|
527
|
+
// later release might ship there. This row is what keeps that from
|
|
528
|
+
// happening.
|
|
529
|
+
absentPaths: [".gemini/", "ts/src/hooks/gemini.ts", "scripts/smoke_gemini_cli.sh"],
|
|
530
|
+
// File and route names, never vocabulary: "gemini" is a living word (the
|
|
531
|
+
// extraction and consult models, Antigravity's own `~/.gemini/` home, the
|
|
532
|
+
// `gemini_cli` label on conversations already logged). The row landed in
|
|
533
|
+
// two steps: US1 carried the names only code and CI used, and
|
|
534
|
+
// CV21.DS8.US2 added the doc-facing ones when it cleared the runtime-facing
|
|
535
|
+
// docs, so no exemption was ever staged. Not `.gemini/settings.json` and
|
|
536
|
+
// not `~/.gemini`: that directory is Antigravity's home too, and the
|
|
537
|
+
// cutoff note names the file a Gemini CLI user edits.
|
|
538
|
+
forbiddenPatterns: [
|
|
539
|
+
"smoke_gemini_cli",
|
|
540
|
+
"geminiHooksSettings",
|
|
541
|
+
"hooks/gemini\\.ts",
|
|
542
|
+
"\\.gemini/hooks",
|
|
543
|
+
"gemini:(session-start|session-end|log-user|log-assistant)",
|
|
544
|
+
"gemini skills link",
|
|
545
|
+
],
|
|
546
|
+
exemptions: {},
|
|
547
|
+
},
|
|
517
548
|
];
|
|
518
549
|
|
|
519
550
|
export interface SurfaceProblem {
|