@rmrdeveloper/sideroom-pi 8.4.0 → 8.5.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/README.md +16 -13
- package/extensions/ask/index.ts +5 -8
- package/extensions/explain/guard.ts +16 -5
- package/extensions/explain/index.ts +2 -2
- package/extensions/explain/model.ts +24 -8
- package/extensions/guidelines/prompt.ts +8 -6
- package/extensions/persona/checks.ts +1 -1
- package/extensions/persona/guard.ts +7 -7
- package/extensions/shared/file-path.ts +83 -0
- package/extensions/todo/index.ts +1 -1
- package/package.json +1 -1
- package/skills/sideroom-grill/SKILL.md +13 -2
- package/skills/sideroom-persona/SKILL.md +6 -3
package/README.md
CHANGED
|
@@ -189,12 +189,12 @@ nothing is detectable, the gate does nothing.
|
|
|
189
189
|
### Explain — the walkthrough offer
|
|
190
190
|
|
|
191
191
|
When an implementation settles, `explain` steers the agent to offer a
|
|
192
|
-
walkthrough through `sideroom_ask`: explain the changes
|
|
193
|
-
only
|
|
192
|
+
walkthrough through `sideroom_ask`: explain the changes only, how to test them
|
|
193
|
+
only, both, or nothing. It fires on `agent_settled`, the point where
|
|
194
194
|
no retry, compaction, or queued message is left, so the offer never lands on
|
|
195
|
-
work that is about to be redone. Once per
|
|
196
|
-
|
|
197
|
-
else. No tool, no widget, no persisted state.
|
|
195
|
+
work that is about to be redone. Once per turn, only when the turn touched at
|
|
196
|
+
least five distinct files, and only in the TUI, because `sideroom_ask` cannot
|
|
197
|
+
run anywhere else. No tool, no widget, no persisted state.
|
|
198
198
|
|
|
199
199
|
## Quick path
|
|
200
200
|
|
|
@@ -292,8 +292,10 @@ the diff against the loaded guide and run the relevant project checks.
|
|
|
292
292
|
- Injection: a short reminder appended in `before_agent_start`, chained after
|
|
293
293
|
the other extensions and idempotent by heading, so it returns after
|
|
294
294
|
compaction.
|
|
295
|
-
- Blocking: decorative symbols in the lines a `write`/`edit` adds.
|
|
296
|
-
|
|
295
|
+
- Blocking: decorative symbols in the lines a `write`/`edit` adds. Full rewrites
|
|
296
|
+
resolve Pi path aliases before comparing existing content; semantic copyright,
|
|
297
|
+
trademark, and registration symbols remain valid. Three fires degrade the
|
|
298
|
+
prohibition to a steer; five clean mutations reset the counters.
|
|
297
299
|
- Steering: one corrective message per violating assistant message, at most
|
|
298
300
|
three per run.
|
|
299
301
|
- `sideroom_persona` takes no arguments and returns every rule and every
|
|
@@ -310,13 +312,14 @@ the diff against the loaded guide and run the relevant project checks.
|
|
|
310
312
|
|
|
311
313
|
### Explain contract
|
|
312
314
|
|
|
313
|
-
- Trigger: `agent_settled` with at least
|
|
314
|
-
last user prompt, in TUI mode.
|
|
315
|
+
- Trigger: `agent_settled` with at least five distinct files mutated since the
|
|
316
|
+
last user prompt, in TUI mode. File identity follows Pi's path aliases and
|
|
317
|
+
canonicalizes existing paths.
|
|
315
318
|
- The steer names the intent; the agent writes the question in the user's
|
|
316
|
-
language with
|
|
317
|
-
*Out of scope* and the custom answer.
|
|
318
|
-
- At most one offer per
|
|
319
|
-
|
|
319
|
+
language with four options, one of them a decline, and one recommendation.
|
|
320
|
+
`sideroom_ask` still adds *Out of scope* and the custom answer.
|
|
321
|
+
- At most one offer per turn. The flag is set before the steer, so the turn the
|
|
322
|
+
offer triggers cannot re-trigger it.
|
|
320
323
|
- No green-check requirement: red checks still produce the offer.
|
|
321
324
|
|
|
322
325
|
### Grill contract
|
package/extensions/ask/index.ts
CHANGED
|
@@ -15,14 +15,11 @@ export const ASK_PROMPT_SNIPPET =
|
|
|
15
15
|
'Ask the user one or more questions with a recommended option, in their language.';
|
|
16
16
|
|
|
17
17
|
export const ASK_PROMPT_GUIDELINES = [
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'sideroom_ask
|
|
23
|
-
'For a genuinely plural sideroom_ask question, set selectionMode "multiple" with recommendedIndices (at least one) instead of recommendationIndex; never send both fields, and keep the single default otherwise.',
|
|
24
|
-
'One sideroom_ask call is one batch; call it again if another round of questions is needed.',
|
|
25
|
-
'sideroom_ask only works in the interactive TUI; it returns an error in print, JSON, or RPC modes.',
|
|
18
|
+
'When a non-trivial request is not yet a settled spec, follow the sideroom-grill skill before implementing; it owns the interview and drives sideroom_ask rounds. One sideroom_ask call is one batch; call it again for another round.',
|
|
19
|
+
'A decision the repository already settles consistently is a project fact: read the code and reuse it. A decision that materially affects architecture, dependencies, cost, security, operations, persistence, public contracts, or future change difficulty is an architectural decision: expose it through sideroom_ask before committing, with the practical consequences of each alternative and one recommendation that carries a one-line justification, never a list of technology names. If the repository shows two inconclusive patterns for the same kind of decision, name the inconsistency in the prompt. Everything else is a minor decision: take it yourself.',
|
|
20
|
+
'Write sideroom_ask prompts, tab labels, option labels, and option descriptions in the language the user is speaking; keep ids and option values in English.',
|
|
21
|
+
'sideroom_ask always adds Out of scope and a write-your-own answer; never send them yourself. A single-selection question needs recommendationIndex; a multiple-selection question needs recommendedIndices and never recommendationIndex.',
|
|
22
|
+
'sideroom_ask only works in the interactive TUI; it errors in print, JSON, and RPC modes.',
|
|
26
23
|
];
|
|
27
24
|
|
|
28
25
|
// Pi loads extensions/*/index.ts through export default.
|
|
@@ -7,10 +7,15 @@ import {
|
|
|
7
7
|
EXPLAIN_OFFER,
|
|
8
8
|
EXPLAIN_OFFER_TYPE,
|
|
9
9
|
type ExplainState,
|
|
10
|
-
|
|
10
|
+
mutationPathKey,
|
|
11
|
+
resetExplainTurn,
|
|
11
12
|
shouldOfferExplanation,
|
|
12
13
|
} from './model.ts';
|
|
13
14
|
|
|
15
|
+
interface FileToolInput {
|
|
16
|
+
readonly path: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
export function registerExplainGuard(
|
|
15
20
|
pi: ExtensionAPI,
|
|
16
21
|
state: ExplainState,
|
|
@@ -19,17 +24,23 @@ export function registerExplainGuard(
|
|
|
19
24
|
if (event.source !== 'interactive' && event.source !== 'rpc') {
|
|
20
25
|
return;
|
|
21
26
|
}
|
|
22
|
-
|
|
27
|
+
resetExplainTurn(state);
|
|
23
28
|
});
|
|
24
29
|
|
|
25
|
-
pi.on('tool_result', (event) => {
|
|
30
|
+
pi.on('tool_result', (event, ctx) => {
|
|
26
31
|
if (event.isError) {
|
|
27
32
|
return;
|
|
28
33
|
}
|
|
29
34
|
if (!isEditToolResult(event) && !isWriteToolResult(event)) {
|
|
30
35
|
return;
|
|
31
36
|
}
|
|
32
|
-
|
|
37
|
+
// SAFETY: Pi validates built-in file-tool inputs before emitting tool_result.
|
|
38
|
+
const input = event.input as unknown as FileToolInput;
|
|
39
|
+
const pathKey = mutationPathKey(ctx.cwd, input.path);
|
|
40
|
+
if (pathKey === undefined) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
state.mutatedFilesThisTurn.add(pathKey);
|
|
33
44
|
});
|
|
34
45
|
|
|
35
46
|
// agent_settled is the only point with no retry, compaction, or queued
|
|
@@ -38,7 +49,7 @@ export function registerExplainGuard(
|
|
|
38
49
|
if (!shouldOfferExplanation(state, ctx.mode)) {
|
|
39
50
|
return;
|
|
40
51
|
}
|
|
41
|
-
state.
|
|
52
|
+
state.offeredThisTurn = true;
|
|
42
53
|
pi.sendMessage(
|
|
43
54
|
{
|
|
44
55
|
customType: EXPLAIN_OFFER_TYPE,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { registerExplainGuard } from './guard.ts';
|
|
3
|
-
import { createExplainState,
|
|
3
|
+
import { createExplainState, resetExplainTurn } from './model.ts';
|
|
4
4
|
|
|
5
5
|
// Pi loads extensions/*/index.ts through export default.
|
|
6
6
|
export default function registerExplain(pi: ExtensionAPI): void {
|
|
7
7
|
const state = createExplainState();
|
|
8
8
|
registerExplainGuard(pi, state);
|
|
9
9
|
|
|
10
|
-
pi.on('session_start', () =>
|
|
10
|
+
pi.on('session_start', () => resetExplainTurn(state));
|
|
11
11
|
}
|
|
@@ -1,22 +1,34 @@
|
|
|
1
|
+
import { resolveFileToolPath } from '../shared/file-path.ts';
|
|
2
|
+
|
|
1
3
|
export const EXPLAIN_OFFER_TYPE = 'sideroom-explain-offer';
|
|
2
4
|
|
|
5
|
+
// Five is the point where a turn stopped being an adjustment and became work
|
|
6
|
+
// worth walking the user through.
|
|
7
|
+
export const EXPLAIN_FILE_THRESHOLD = 5;
|
|
8
|
+
|
|
3
9
|
const TUI_MODE = 'tui';
|
|
4
10
|
|
|
5
11
|
export const EXPLAIN_OFFER =
|
|
6
|
-
"Use sideroom_ask for one question in the user's language with
|
|
12
|
+
"Use sideroom_ask for one question in the user's language with four parallel options: changes only, test steps only, both, or no explanation. Recommend changes only.";
|
|
7
13
|
|
|
8
14
|
export interface ExplainState {
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
mutatedFilesThisTurn: Set<string>;
|
|
16
|
+
offeredThisTurn: boolean;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
export function createExplainState(): ExplainState {
|
|
14
|
-
return {
|
|
20
|
+
return { mutatedFilesThisTurn: new Set(), offeredThisTurn: false };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resetExplainTurn(state: ExplainState): void {
|
|
24
|
+
state.mutatedFilesThisTurn.clear();
|
|
25
|
+
state.offeredThisTurn = false;
|
|
15
26
|
}
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
// Pi reports the path as the model wrote it, so use the built-in file-tool
|
|
29
|
+
// semantics and canonicalize existing aliases before counting the file.
|
|
30
|
+
export function mutationPathKey(cwd: string, path: string): string | undefined {
|
|
31
|
+
return resolveFileToolPath(cwd, path);
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
// sideroom_ask rejects every mode but the TUI, so the offer is gated the same way.
|
|
@@ -24,5 +36,9 @@ export function shouldOfferExplanation(
|
|
|
24
36
|
state: ExplainState,
|
|
25
37
|
mode: string,
|
|
26
38
|
): boolean {
|
|
27
|
-
return
|
|
39
|
+
return (
|
|
40
|
+
mode === TUI_MODE &&
|
|
41
|
+
state.mutatedFilesThisTurn.size >= EXPLAIN_FILE_THRESHOLD &&
|
|
42
|
+
!state.offeredThisTurn
|
|
43
|
+
);
|
|
28
44
|
}
|
|
@@ -5,14 +5,16 @@ export { LANGUAGE_GUIDE_FILES } from './catalog.ts';
|
|
|
5
5
|
export const GUIDELINES_REMINDER_HEADING =
|
|
6
6
|
'Sideroom coding guidelines (always-on reminder):';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
).join('
|
|
8
|
+
// One dense index instead of one sentence per language: the same paths and the
|
|
9
|
+
// same requirement, a fraction of the tokens every turn.
|
|
10
|
+
const LANGUAGE_GUIDE_INDEX = LANGUAGE_GUIDES.map(
|
|
11
|
+
({ extensions, path }) => `${extensions.join('/')} ${path}`,
|
|
12
|
+
).join('; ');
|
|
12
13
|
|
|
13
14
|
export const GUIDELINES_REMINDER = `${GUIDELINES_REMINDER_HEADING}
|
|
14
|
-
Before the first write or edit in each agent run, use the read tool without offset or limit to load
|
|
15
|
-
${
|
|
15
|
+
Before the first write or edit in each agent run, use the read tool without offset or limit to load these files in full:
|
|
16
|
+
- ${GUIDELINE_SKILL_PATH}
|
|
17
|
+
- for the extension of the file you will change, one of: ${LANGUAGE_GUIDE_INDEX}
|
|
16
18
|
A required read counts only when it succeeds without truncation. Until all required reads succeed, do not call write or edit and do not mutate files through bash or another tool; the extension blocks those mutations.
|
|
17
19
|
After changing files, review the code against the loaded guide and run the relevant formatter, linter, type checks, and tests.`;
|
|
18
20
|
|
|
@@ -16,7 +16,7 @@ interface ProhibitionDetector {
|
|
|
16
16
|
const DETECTORS: readonly ProhibitionDetector[] = [
|
|
17
17
|
{
|
|
18
18
|
prohibitionId: 'decorative-symbols',
|
|
19
|
-
pattern: /\p{
|
|
19
|
+
pattern: /\p{Emoji_Presentation}|\uFE0F|[\u2713\u2714\u2717\u2718]/u,
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
prohibitionId: 'flattery-and-filler',
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
2
|
import {
|
|
4
3
|
type ExtensionAPI,
|
|
5
4
|
isToolCallEventType,
|
|
6
5
|
type ToolCallEvent,
|
|
7
6
|
} from '@earendil-works/pi-coding-agent';
|
|
7
|
+
import { resolveFileToolPath } from '../shared/file-path.ts';
|
|
8
8
|
import {
|
|
9
9
|
PERSONA_STEER_TYPE,
|
|
10
10
|
type ProhibitionId,
|
|
@@ -107,7 +107,11 @@ function extractMutation(
|
|
|
107
107
|
cwd: string,
|
|
108
108
|
): ExtractedMutation | undefined {
|
|
109
109
|
if (isToolCallEventType('write', event)) {
|
|
110
|
-
const
|
|
110
|
+
const absolutePath = resolveFileToolPath(cwd, event.input.path);
|
|
111
|
+
if (absolutePath === undefined) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const previous = readFileIfExists(absolutePath);
|
|
111
115
|
return {
|
|
112
116
|
path: event.input.path,
|
|
113
117
|
addedLines: addedLinesMissingFrom(previous, event.input.content),
|
|
@@ -126,17 +130,13 @@ function readFileIfExists(path: string): string | undefined {
|
|
|
126
130
|
try {
|
|
127
131
|
return readFileSync(path, 'utf8');
|
|
128
132
|
} catch (error) {
|
|
129
|
-
if (
|
|
133
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
130
134
|
return undefined;
|
|
131
135
|
}
|
|
132
136
|
throw error;
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
139
|
|
|
136
|
-
function isMissingFile(error: unknown): boolean {
|
|
137
|
-
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
140
|
function isDegraded(state: PersonaGuardState, id: ProhibitionId): boolean {
|
|
141
141
|
return (state.fires.get(id) ?? 0) >= BLOCK_DEGRADE_AFTER;
|
|
142
142
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const FILE_URL_PREFIX = 'file://';
|
|
7
|
+
const MISSING_PATH_ERROR_CODE = {
|
|
8
|
+
file: 'ENOENT',
|
|
9
|
+
directory: 'ENOTDIR',
|
|
10
|
+
} as const;
|
|
11
|
+
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
12
|
+
|
|
13
|
+
// File guards must resolve the model's path exactly as Pi's built-in tools do.
|
|
14
|
+
export function resolveFileToolPath(
|
|
15
|
+
cwd: string,
|
|
16
|
+
path: string,
|
|
17
|
+
): string | undefined {
|
|
18
|
+
const normalizedPath = normalizeFileToolPath(path);
|
|
19
|
+
if (normalizedPath.length === 0) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const localPath = normalizedPath.startsWith(FILE_URL_PREFIX)
|
|
24
|
+
? fileURLToPath(normalizedPath)
|
|
25
|
+
: normalizedPath;
|
|
26
|
+
const absolutePath = isAbsolute(localPath)
|
|
27
|
+
? resolve(localPath)
|
|
28
|
+
: resolve(cwd, localPath);
|
|
29
|
+
return realpathOr(absolutePath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeFileToolPath(path: string): string {
|
|
33
|
+
const normalizedSpaces = path.replace(UNICODE_SPACES, ' ');
|
|
34
|
+
const withoutAtPrefix = normalizedSpaces.startsWith('@')
|
|
35
|
+
? normalizedSpaces.slice(1)
|
|
36
|
+
: normalizedSpaces;
|
|
37
|
+
return expandHome(normalizeWindowsShellPath(withoutAtPrefix));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeWindowsShellPath(path: string): string {
|
|
41
|
+
if (
|
|
42
|
+
process.platform !== 'win32' ||
|
|
43
|
+
!path.startsWith('/') ||
|
|
44
|
+
path.startsWith('//') ||
|
|
45
|
+
path.includes('\\')
|
|
46
|
+
) {
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const match = path.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
|
|
51
|
+
const drive = match?.[1];
|
|
52
|
+
if (drive === undefined) {
|
|
53
|
+
return path;
|
|
54
|
+
}
|
|
55
|
+
const suffix = match?.[2]?.replaceAll('/', '\\') ?? '';
|
|
56
|
+
return `${drive.toUpperCase()}:\\${suffix}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function expandHome(path: string): string {
|
|
60
|
+
if (path === '~') {
|
|
61
|
+
return homedir();
|
|
62
|
+
}
|
|
63
|
+
if (path.startsWith('~/') || path.startsWith('~\\')) {
|
|
64
|
+
return join(homedir(), path.slice(2));
|
|
65
|
+
}
|
|
66
|
+
return path;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function realpathOr(path: string): string {
|
|
70
|
+
try {
|
|
71
|
+
return realpathSync.native(path);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (
|
|
74
|
+
error instanceof Error &&
|
|
75
|
+
'code' in error &&
|
|
76
|
+
(error.code === MISSING_PATH_ERROR_CODE.file ||
|
|
77
|
+
error.code === MISSING_PATH_ERROR_CODE.directory)
|
|
78
|
+
) {
|
|
79
|
+
return path;
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
package/extensions/todo/index.ts
CHANGED
|
@@ -28,7 +28,7 @@ export const TODO_PROMPT_SNIPPET =
|
|
|
28
28
|
|
|
29
29
|
export const TODO_PROMPT_GUIDELINES = [
|
|
30
30
|
'Use sideroom_todo to maintain a live work board when implementation has more than one step.',
|
|
31
|
-
'
|
|
31
|
+
'While any item is pending, exactly one must be in_progress.',
|
|
32
32
|
'Complete the current item and start the next item in the same update call.',
|
|
33
33
|
'propose is interactive-TUI-only; update also works in print, JSON, and RPC modes.',
|
|
34
34
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmrdeveloper/sideroom-pi",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.5.0",
|
|
4
4
|
"description": "Pi coding-agent package that asks clarifying questions, shows a live todo board, enforces coding guidelines, and will not call it done while your checks are red.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -25,6 +25,15 @@ if the effort is too big to hold in one session, say so and stop.
|
|
|
25
25
|
in the language the user is speaking; keep ids and option values in English.
|
|
26
26
|
- Never ask the user what the codebase can answer. Read the code first; ask
|
|
27
27
|
only what the code cannot settle.
|
|
28
|
+
- Classify before asking. A project fact is read from the code and reused. A
|
|
29
|
+
minor decision is taken without interrupting. A decision that materially
|
|
30
|
+
affects architecture, external dependencies, cost, security, operations,
|
|
31
|
+
persistence, public contracts, or future change difficulty is architectural
|
|
32
|
+
and must be exposed.
|
|
33
|
+
- Present an architectural decision as the current situation, the alternatives
|
|
34
|
+
by their practical consequences, and one recommendation with a one-line
|
|
35
|
+
justification. Never reduce it to technology names. Name a repository
|
|
36
|
+
inconsistency in the prompt instead of picking one.
|
|
28
37
|
- An *Out of scope* answer closes that thread. Do not re-ask it.
|
|
29
38
|
- Everything a round settles is recorded through `sideroom-domain-modeling`:
|
|
30
39
|
inline glossary entries, the ADR three-gate offer, lazy file creation.
|
|
@@ -38,9 +47,11 @@ if the effort is too big to hold in one session, say so and stop.
|
|
|
38
47
|
1. Read the code around the change. Settle from the codebase everything the
|
|
39
48
|
codebase settles.
|
|
40
49
|
2. Run one `sideroom_ask` round on what remains genuinely open: terms first,
|
|
41
|
-
then decisions.
|
|
50
|
+
then the architectural decisions. Minor decisions are yours; do not spend a
|
|
51
|
+
round on them.
|
|
42
52
|
3. After each round, write resolved terms to `CONTEXT.md` and qualifying
|
|
43
|
-
decisions to `docs/adr/`.
|
|
53
|
+
decisions to `docs/adr/`. Offer the ADR for an accepted architectural
|
|
54
|
+
decision only when all three gates pass.
|
|
44
55
|
4. Repeat until no open term or hard decision remains, or the user cancels.
|
|
45
56
|
5. Close by stating the settled glossary, the ADRs written (if any), and the
|
|
46
57
|
suggested next step: a spec for anything non-trivial, implementation for
|
|
@@ -62,9 +62,12 @@ until they ask for another.
|
|
|
62
62
|
|
|
63
63
|
A `write` or `edit` whose **added lines** contain a decorative symbol is
|
|
64
64
|
rejected with a reason listing the offending line. Existing content is not
|
|
65
|
-
re-checked,
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
re-checked, including when the model uses Pi path aliases for a full rewrite.
|
|
66
|
+
The detector targets default emoji presentation, emoji variation selectors,
|
|
67
|
+
and explicit check or cross marks; semantic copyright, trademark, and
|
|
68
|
+
registration symbols stay valid.
|
|
69
|
+
A prohibition that fires three times degrades to a steer so the agent is never
|
|
70
|
+
dead-locked.
|
|
68
71
|
|
|
69
72
|
### What steering means
|
|
70
73
|
|