@miphamai/cli 0.24.16 → 0.26.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/package.json +1 -1
- package/skills/standard/codebase-design.SKILL.md +189 -0
- package/skills/standard/domain-modeling.SKILL.md +129 -0
- package/skills/standard/grill-with-docs.SKILL.md +199 -0
- package/skills/standard/to-spec.SKILL.md +138 -0
- package/skills/standard/triage.SKILL.md +155 -0
- package/src/agent/message-bus.ts +5 -0
- package/src/agent/sub-agent.ts +99 -4
- package/src/agent/types.ts +2 -1
- package/src/core/engine.ts +50 -3
- package/src/core/instructions.ts +70 -1
- package/src/core/permission.ts +92 -0
- package/src/index.tsx +9 -2
- package/src/shared/sanitize.ts +182 -0
- package/src/tools/agent/send-message.ts +5 -2
- package/src/tools/exec/bash.ts +40 -3
- package/src/tools/exec/git.ts +71 -4
- package/src/ui/app.tsx +8 -3
- package/src/workflow/primitives/parallel.ts +55 -2
- package/src/workflow/primitives/pipeline.ts +58 -9
- package/src/workflow/sandbox.ts +64 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: triage
|
|
3
|
+
description: Structured task decomposition and tracking across sessions. Use for breaking complex plans into trackable tickets with dependency graphs, checking task status, or continuing work from a previous session.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
user-invocable: true
|
|
6
|
+
allowed-tools:
|
|
7
|
+
- Read
|
|
8
|
+
- Write
|
|
9
|
+
- Edit
|
|
10
|
+
- Bash
|
|
11
|
+
- Glob
|
|
12
|
+
- Grep
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Triage — Cross-Session Task Tracking
|
|
16
|
+
|
|
17
|
+
Turn plans into trackable tickets with dependency management. Inspired by Matt Pocock's `triage` + `to-tickets` + `wayfinder` skills, consolidated into one Mipham Code skill.
|
|
18
|
+
|
|
19
|
+
## When to Use
|
|
20
|
+
|
|
21
|
+
- Breaking a large plan into actionable tickets
|
|
22
|
+
- Tracking work across multiple sessions
|
|
23
|
+
- User asks: "what's next?", "where did I leave off?", "what's the status?"
|
|
24
|
+
- Complex tasks with dependencies between them
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## The Ticket Format
|
|
29
|
+
|
|
30
|
+
Tickets live in `.mipham/tickets/` as individual Markdown files:
|
|
31
|
+
|
|
32
|
+
```markdown
|
|
33
|
+
---
|
|
34
|
+
id: T-001
|
|
35
|
+
title: Add user authentication
|
|
36
|
+
status: in-progress
|
|
37
|
+
priority: P0
|
|
38
|
+
depends_on: []
|
|
39
|
+
blocks: [T-003]
|
|
40
|
+
created: 2026-08-10
|
|
41
|
+
tags:
|
|
42
|
+
- auth
|
|
43
|
+
- backend
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Description
|
|
47
|
+
|
|
48
|
+
Add JWT-based authentication with refresh token rotation.
|
|
49
|
+
|
|
50
|
+
## Acceptance Criteria
|
|
51
|
+
|
|
52
|
+
- [ ] Login endpoint returns access + refresh tokens
|
|
53
|
+
- [ ] Refresh endpoint rotates tokens
|
|
54
|
+
- [ ] Invalid tokens return 401
|
|
55
|
+
- [ ] Rate limiting on login attempts
|
|
56
|
+
|
|
57
|
+
## Notes
|
|
58
|
+
|
|
59
|
+
- OAuth not in scope for T-001 (punted to T-005)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Status Values
|
|
63
|
+
|
|
64
|
+
| Status | Meaning |
|
|
65
|
+
| ------------- | ------------------------------------------ |
|
|
66
|
+
| `backlog` | Not yet planned for any session |
|
|
67
|
+
| `planned` | Scoped and ready to work |
|
|
68
|
+
| `in-progress` | Currently being worked on |
|
|
69
|
+
| `review` | Implementation done, awaiting verification |
|
|
70
|
+
| `done` | Verified and merged |
|
|
71
|
+
| `blocked` | Cannot proceed due to dependency |
|
|
72
|
+
| `wontfix` | Decided not to do |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## The Triage Workflow
|
|
77
|
+
|
|
78
|
+
### Phase 1: Decompose (Plan → Tickets)
|
|
79
|
+
|
|
80
|
+
Given a plan or feature request:
|
|
81
|
+
|
|
82
|
+
1. **Identify the smallest independently-valuable units of work**
|
|
83
|
+
- Each ticket should deliver value on its own
|
|
84
|
+
- If a ticket requires 3+ files touched, it's probably too big
|
|
85
|
+
- If a ticket can be done in < 15 minutes, it's probably too small
|
|
86
|
+
|
|
87
|
+
2. **Map dependencies**
|
|
88
|
+
- What must be done first? (hard dependency)
|
|
89
|
+
- What would be easier after something else? (soft dependency)
|
|
90
|
+
- What blocks other work? (reverse dependency)
|
|
91
|
+
|
|
92
|
+
3. **Assign priorities**
|
|
93
|
+
- **P0**: Blocks other work, must do first
|
|
94
|
+
- **P1**: High value, should do soon
|
|
95
|
+
- **P2**: Nice to have, can defer
|
|
96
|
+
- **P3**: Optional, do if time permits
|
|
97
|
+
|
|
98
|
+
4. **Write acceptance criteria**
|
|
99
|
+
- Specific, testable, unambiguous
|
|
100
|
+
- "Login works" is bad. "POST /auth/login with valid credentials returns 200 + JWT" is good.
|
|
101
|
+
|
|
102
|
+
### Phase 2: Status Check
|
|
103
|
+
|
|
104
|
+
When the user asks "what's next?" or "what's the status?":
|
|
105
|
+
|
|
106
|
+
1. Read `.mipham/tickets/` directory
|
|
107
|
+
2. Report:
|
|
108
|
+
- Currently in-progress tickets
|
|
109
|
+
- Blocked tickets (and what's blocking them)
|
|
110
|
+
- Next unblocked P0/P1 tickets ready to work
|
|
111
|
+
- Recently completed tickets (for context)
|
|
112
|
+
|
|
113
|
+
### Phase 3: Session Handoff
|
|
114
|
+
|
|
115
|
+
When starting a new session, check for continuity:
|
|
116
|
+
|
|
117
|
+
1. Read the previous session's context from the session store
|
|
118
|
+
2. Check ticket statuses — any that were `in-progress` last session?
|
|
119
|
+
3. Present: "Last session you were working on T-004 (Add rate limiting). Continue from there, or start on T-007 (API docs) which is next in the P1 queue?"
|
|
120
|
+
|
|
121
|
+
### Phase 4: Ticket Lifecycle
|
|
122
|
+
|
|
123
|
+
When working on a ticket:
|
|
124
|
+
|
|
125
|
+
- Mark it `in-progress` when you start
|
|
126
|
+
- Mark it `review` when implementation is done
|
|
127
|
+
- Mark it `done` after verification (tests pass, typecheck clean)
|
|
128
|
+
- If you discover new dependencies, add them to `blocks`/`depends_on`
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Dependency Graph
|
|
133
|
+
|
|
134
|
+
For tickets with complex dependencies, generate a visual summary:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
T-001 (Auth) ──blocks──→ T-003 (Dashboard)
|
|
138
|
+
│ │
|
|
139
|
+
└──blocks──→ T-002 (API) ─┘
|
|
140
|
+
│
|
|
141
|
+
└──soft-dep──→ T-004 (Rate Limiting)
|
|
142
|
+
|
|
143
|
+
Ready to work: T-001 (no dependencies)
|
|
144
|
+
Blocked: T-002 (waiting on T-001), T-003 (waiting on T-001, T-002)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Integration With Mipham Code
|
|
150
|
+
|
|
151
|
+
- **Session Store**: Ticket status persists across sessions via `.mipham/tickets/`
|
|
152
|
+
- **Memory System**: Active tickets are loaded as project memory for context
|
|
153
|
+
- **grill-with-docs**: The output of a grill session feeds directly into ticket decomposition
|
|
154
|
+
- **Background Agents**: Long-running work on a ticket can be spawned as a background agent
|
|
155
|
+
- **Critical Thinking Layer**: When decomposing, ask "what's the smallest thing that delivers value?" — don't over-decompose
|
package/src/agent/message-bus.ts
CHANGED
|
@@ -56,15 +56,20 @@ export class AgentMessageBus {
|
|
|
56
56
|
/**
|
|
57
57
|
* Get all unread messages addressed to the given agent.
|
|
58
58
|
* Does NOT mark them as read — use markRead() for that.
|
|
59
|
+
* P2-3: Auto-prunes messages older than 1 hour before polling.
|
|
59
60
|
*/
|
|
60
61
|
poll(agentId: string): AgentMessage[] {
|
|
62
|
+
// Auto-prune stale messages to prevent accumulation and stuck states
|
|
63
|
+
this.prune(60 * 60 * 1000) // 1 hour TTL
|
|
61
64
|
return this.messages.filter((m) => m.to === agentId && !m.read)
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
/**
|
|
65
68
|
* Get all messages addressed to the given agent (read + unread).
|
|
69
|
+
* P2-3: Auto-prunes stale messages before listing.
|
|
66
70
|
*/
|
|
67
71
|
list(agentId: string): AgentMessage[] {
|
|
72
|
+
this.prune(60 * 60 * 1000) // 1 hour TTL
|
|
68
73
|
return this.messages.filter((m) => m.to === agentId)
|
|
69
74
|
}
|
|
70
75
|
|
package/src/agent/sub-agent.ts
CHANGED
|
@@ -198,6 +198,22 @@ export class SubAgent {
|
|
|
198
198
|
const agentType = options.type || 'general'
|
|
199
199
|
const agentDef = options.agentDef
|
|
200
200
|
|
|
201
|
+
// ── P0-3: Create isolated PermissionSystem for this sub-agent ──
|
|
202
|
+
// The agent definition's permissionMode is clamped against parent's org
|
|
203
|
+
// restrictions (maxAllowedMode, forbiddenModes). When permissionMode is
|
|
204
|
+
// 'inherit' or not set, the parent's mode is used but still clamped.
|
|
205
|
+
let subPermission = this.permission // default: use parent's
|
|
206
|
+
if (this.permission) {
|
|
207
|
+
const agentPermMode =
|
|
208
|
+
agentDef?.permissionMode && agentDef.permissionMode !== 'inherit'
|
|
209
|
+
? agentDef.permissionMode
|
|
210
|
+
: 'inherit'
|
|
211
|
+
subPermission = this.permission.createSubAgentPermission(agentPermMode)
|
|
212
|
+
// P0-5: Enable sub-agent safety mode — auto mode returns 'ask' for
|
|
213
|
+
// Bash/Write/Edit so hooks remain the safety gate in background agents.
|
|
214
|
+
subPermission.setSubAgentMode(true)
|
|
215
|
+
}
|
|
216
|
+
|
|
201
217
|
// Resolve execution directory: worktree isolation or process cwd
|
|
202
218
|
const execCwd = options.worktreePath || process.cwd()
|
|
203
219
|
|
|
@@ -212,6 +228,25 @@ export class SubAgent {
|
|
|
212
228
|
}
|
|
213
229
|
// Otherwise, proceed with a warning — don't block agent execution
|
|
214
230
|
}
|
|
231
|
+
|
|
232
|
+
// P0-4: Auto-disallow Git tool for worktree-isolated sub-agents
|
|
233
|
+
// to prevent destructive operations on the main checkout.
|
|
234
|
+
// Agent definitions can explicitly re-enable Git if needed.
|
|
235
|
+
if (!agentDef?.tools) {
|
|
236
|
+
// No explicit allowlist — add Git to disallowedTools
|
|
237
|
+
const existingDisallowed = agentDef?.disallowedTools
|
|
238
|
+
? agentDef.disallowedTools
|
|
239
|
+
.split(',')
|
|
240
|
+
.map((s) => s.trim())
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
: []
|
|
243
|
+
if (!existingDisallowed.includes('Git')) {
|
|
244
|
+
existingDisallowed.push('Git')
|
|
245
|
+
}
|
|
246
|
+
if (agentDef) {
|
|
247
|
+
agentDef.disallowedTools = existingDisallowed.join(',')
|
|
248
|
+
}
|
|
249
|
+
}
|
|
215
250
|
}
|
|
216
251
|
|
|
217
252
|
// Resolve system prompt: agentDef > options.systemPrompt > builtin type
|
|
@@ -352,11 +387,36 @@ export class SubAgent {
|
|
|
352
387
|
continue
|
|
353
388
|
}
|
|
354
389
|
|
|
390
|
+
// ── P0-5: Run PreToolUse hooks (before permission check) ──
|
|
391
|
+
let effectiveInput = tu.input
|
|
392
|
+
if (this.hookEngine) {
|
|
393
|
+
const preResult = await this.hookEngine.executePreToolUse(
|
|
394
|
+
tu.name,
|
|
395
|
+
tu.input,
|
|
396
|
+
'sub-agent',
|
|
397
|
+
)
|
|
398
|
+
if (!preResult.allowed) {
|
|
399
|
+
const denialMsg = preResult.reason
|
|
400
|
+
? `Tool "${tu.name}" blocked by PreToolUse hook: ${preResult.reason}`
|
|
401
|
+
: `Tool "${tu.name}" blocked by PreToolUse hook.`
|
|
402
|
+
currentMessages.push({
|
|
403
|
+
role: 'user' as const,
|
|
404
|
+
content: denialMsg,
|
|
405
|
+
})
|
|
406
|
+
continue
|
|
407
|
+
}
|
|
408
|
+
// Apply modified input from hooks
|
|
409
|
+
if (preResult.modifiedInput) {
|
|
410
|
+
effectiveInput = { ...tu.input, ...preResult.modifiedInput }
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
355
414
|
// Security: check permission before executing.
|
|
356
415
|
// Sub-agents run without user interaction — tools requiring approval are rejected.
|
|
357
416
|
// When permission system is absent (undefined), allow all tools (backward compat
|
|
358
417
|
// for tests and headless usage). When present, always enforce approval checks.
|
|
359
|
-
|
|
418
|
+
// P0-3: Uses isolated subPermission (clamped by org restrictions) instead of parent's.
|
|
419
|
+
if (subPermission?.needsApproval(tool, effectiveInput)) {
|
|
360
420
|
currentMessages.push({
|
|
361
421
|
role: 'user' as const,
|
|
362
422
|
content:
|
|
@@ -367,16 +427,40 @@ export class SubAgent {
|
|
|
367
427
|
}
|
|
368
428
|
|
|
369
429
|
try {
|
|
370
|
-
const result = await tool.execute(
|
|
430
|
+
const result = await tool.execute(effectiveInput, {
|
|
371
431
|
cwd: execCwd,
|
|
372
432
|
sessionId: 'sub-agent',
|
|
373
433
|
provider: '',
|
|
374
434
|
model: finalModel,
|
|
375
435
|
})
|
|
376
436
|
|
|
437
|
+
// ── P0-5: Run PostToolUse hooks ──
|
|
438
|
+
let displayResult = result
|
|
439
|
+
if (this.hookEngine) {
|
|
440
|
+
const postResult = await this.hookEngine.executePostToolUse(
|
|
441
|
+
tu.name,
|
|
442
|
+
effectiveInput,
|
|
443
|
+
result,
|
|
444
|
+
'sub-agent',
|
|
445
|
+
)
|
|
446
|
+
if (postResult.updatedOutput) {
|
|
447
|
+
displayResult = { ...result, content: postResult.updatedOutput }
|
|
448
|
+
}
|
|
449
|
+
if (postResult.additionalContext) {
|
|
450
|
+
displayResult = {
|
|
451
|
+
...displayResult,
|
|
452
|
+
content: displayResult.content
|
|
453
|
+
? displayResult.content + '\n' + postResult.additionalContext
|
|
454
|
+
: postResult.additionalContext,
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
377
459
|
currentMessages.push({
|
|
378
460
|
role: 'assistant' as const,
|
|
379
|
-
content: [
|
|
461
|
+
content: [
|
|
462
|
+
{ type: 'tool_use' as const, id: tu.id, name: tu.name, input: effectiveInput },
|
|
463
|
+
],
|
|
380
464
|
})
|
|
381
465
|
currentMessages.push({
|
|
382
466
|
role: 'user' as const,
|
|
@@ -384,11 +468,22 @@ export class SubAgent {
|
|
|
384
468
|
{
|
|
385
469
|
type: 'tool_result' as const,
|
|
386
470
|
tool_use_id: tu.id,
|
|
387
|
-
content:
|
|
471
|
+
content: displayResult.success
|
|
472
|
+
? displayResult.content
|
|
473
|
+
: displayResult.error || displayResult.content,
|
|
388
474
|
},
|
|
389
475
|
],
|
|
390
476
|
})
|
|
391
477
|
} catch (err) {
|
|
478
|
+
// ── P0-5: Run PostToolUseFailure hooks ──
|
|
479
|
+
if (this.hookEngine) {
|
|
480
|
+
this.hookEngine
|
|
481
|
+
.executePostToolUseFailure(tu.name, effectiveInput, String(err), 'sub-agent')
|
|
482
|
+
.catch(() => {
|
|
483
|
+
// Hook failures never block execution
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
392
487
|
currentMessages.push({
|
|
393
488
|
role: 'user' as const,
|
|
394
489
|
content: `Tool "${tu.name}" execution error: ${String(err)}`,
|
package/src/agent/types.ts
CHANGED
|
@@ -8,7 +8,8 @@ export interface AgentFrontmatter {
|
|
|
8
8
|
tools?: string // comma-separated allowlist
|
|
9
9
|
disallowedTools?: string
|
|
10
10
|
model?: string // 'sonnet' | 'opus' | 'haiku' | 'inherit' | full model ID
|
|
11
|
-
permissionMode?:
|
|
11
|
+
permissionMode?:
|
|
12
|
+
'default' | 'acceptEdits' | 'auto' | 'bypass' | 'plan' | 'bypassPermissions' | 'dontAsk'
|
|
12
13
|
maxTurns?: number
|
|
13
14
|
skills?: string
|
|
14
15
|
background?: boolean
|
package/src/core/engine.ts
CHANGED
|
@@ -112,6 +112,16 @@ export class QueryEngine {
|
|
|
112
112
|
if (messages.length > 0) {
|
|
113
113
|
const bus = getMessageBus()
|
|
114
114
|
for (const msg of messages) {
|
|
115
|
+
// P2-1: Trigger Notification hook for cross-session messages
|
|
116
|
+
if (this.hookEngine) {
|
|
117
|
+
this.hookEngine
|
|
118
|
+
.executeNotification(
|
|
119
|
+
`Cross-session message from ${msg.from}: ${msg.summary}`,
|
|
120
|
+
this.sessionId,
|
|
121
|
+
)
|
|
122
|
+
.catch(() => {})
|
|
123
|
+
}
|
|
124
|
+
|
|
115
125
|
if (policy === 'ask') {
|
|
116
126
|
// Mark as awaiting approval — the model should verify with the user before acting
|
|
117
127
|
bus.post(
|
|
@@ -836,17 +846,31 @@ export class QueryEngine {
|
|
|
836
846
|
private async executeTool(name: string, params: Record<string, unknown>): Promise<ToolResult> {
|
|
837
847
|
const tool = this.tools.get(name)
|
|
838
848
|
if (!tool) {
|
|
839
|
-
|
|
849
|
+
// P2-4: Provide a more helpful error for unavailable tools
|
|
850
|
+
const isMcpTool = name.startsWith('mcp__')
|
|
851
|
+
const hint = isMcpTool
|
|
852
|
+
? `\nMCP tool "${name}" is no longer available. The MCP server may have been removed or disconnected. Use tool-search to discover available tools.`
|
|
853
|
+
: `\nTool "${name}" is not registered. Available tools may have changed — check the tool list.`
|
|
854
|
+
return { success: false, content: '', error: `Unknown tool: ${name}${hint}` }
|
|
840
855
|
}
|
|
841
856
|
|
|
842
857
|
// Security: check permission before executing
|
|
843
858
|
if (this.permission.needsApproval(tool, params)) {
|
|
859
|
+
// P1-4: Increment consecutive block counter; if limit exceeded,
|
|
860
|
+
// tell the model to move on instead of retrying.
|
|
861
|
+
const limitExceeded = this.permission.incrementBlockCounter()
|
|
862
|
+
const baseError = t('errors.tool_not_allowed', { name })
|
|
863
|
+
const moveOnHint = limitExceeded
|
|
864
|
+
? '\n(Consecutive block limit reached. Please try a different approach or ask the user for guidance.)'
|
|
865
|
+
: ''
|
|
844
866
|
return {
|
|
845
867
|
success: false,
|
|
846
868
|
content: '',
|
|
847
|
-
error:
|
|
869
|
+
error: baseError + moveOnHint,
|
|
848
870
|
}
|
|
849
871
|
}
|
|
872
|
+
// P1-4: Tool allowed — reset block counter
|
|
873
|
+
this.permission.resetBlockCounter()
|
|
850
874
|
|
|
851
875
|
// Run PreToolUse hooks
|
|
852
876
|
let effectiveParams = params
|
|
@@ -902,7 +926,21 @@ export class QueryEngine {
|
|
|
902
926
|
|
|
903
927
|
// Run PostToolUse hooks
|
|
904
928
|
if (this.hookEngine) {
|
|
905
|
-
await this.hookEngine.executePostToolUse(
|
|
929
|
+
const postResult = await this.hookEngine.executePostToolUse(
|
|
930
|
+
name,
|
|
931
|
+
effectiveParams,
|
|
932
|
+
result,
|
|
933
|
+
this.sessionId,
|
|
934
|
+
)
|
|
935
|
+
// P1-2: Consume hook result — apply updatedOutput and additionalContext
|
|
936
|
+
if (postResult.updatedOutput) {
|
|
937
|
+
result.content = postResult.updatedOutput
|
|
938
|
+
}
|
|
939
|
+
if (postResult.additionalContext) {
|
|
940
|
+
result.content = result.content
|
|
941
|
+
? result.content + '\n' + postResult.additionalContext
|
|
942
|
+
: postResult.additionalContext
|
|
943
|
+
}
|
|
906
944
|
}
|
|
907
945
|
|
|
908
946
|
// CRSI: Track rule effectiveness after tool execution
|
|
@@ -920,6 +958,15 @@ export class QueryEngine {
|
|
|
920
958
|
|
|
921
959
|
return result
|
|
922
960
|
} catch (err) {
|
|
961
|
+
// P1-3: Trigger PostToolUseFailure hook on tool execution errors
|
|
962
|
+
if (this.hookEngine) {
|
|
963
|
+
this.hookEngine
|
|
964
|
+
.executePostToolUseFailure(name, effectiveParams, String(err), this.sessionId)
|
|
965
|
+
.catch(() => {
|
|
966
|
+
// Hook failures never block error handling
|
|
967
|
+
})
|
|
968
|
+
}
|
|
969
|
+
|
|
923
970
|
// Sanitize error: strip stack traces and internal paths to prevent
|
|
924
971
|
// information disclosure to the LLM conversation context.
|
|
925
972
|
const message =
|
package/src/core/instructions.ts
CHANGED
|
@@ -47,7 +47,7 @@ export class InstructionsLoader {
|
|
|
47
47
|
this.tryLoad(join(home, '.mipham', 'USER.md'), 'user')
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
buildSystemPrompt(): string {
|
|
50
|
+
buildSystemPrompt(permissionMode?: string): string {
|
|
51
51
|
const parts: string[] = []
|
|
52
52
|
|
|
53
53
|
for (const inst of this.instructions) {
|
|
@@ -61,11 +61,56 @@ export class InstructionsLoader {
|
|
|
61
61
|
parts.push(`<!-- ${levelLabel[inst.level] || inst.level} (${inst.path}) -->\n${inst.content}`)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// P2-2: Inject current permission mode so the model knows its constraints
|
|
65
|
+
if (permissionMode) {
|
|
66
|
+
parts.push(this.buildPermissionContext(permissionMode))
|
|
67
|
+
}
|
|
68
|
+
|
|
64
69
|
// Append skills reminder after all instructions
|
|
65
70
|
if (this.skillsReminder) {
|
|
66
71
|
parts.push(this.skillsReminder)
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
// Inject critical thinking self-check layer (for analysis/comparison tasks)
|
|
75
|
+
parts.push(`## Critical Thinking Self-Check
|
|
76
|
+
|
|
77
|
+
Before delivering any analysis, comparison, evaluation, or "X vs Y"
|
|
78
|
+
report, run this checklist internally:
|
|
79
|
+
|
|
80
|
+
### 1. Evidence Standard
|
|
81
|
+
- Every factual claim MUST cite a specific source (file path, URL, line number)
|
|
82
|
+
- If you cannot cite a source, label the claim as [推断] (inference) or [待验证] (unverified)
|
|
83
|
+
- Numbers (counts, percentages, download stats) require cross-validation from a second source
|
|
84
|
+
|
|
85
|
+
### 2. Equivalence Verification
|
|
86
|
+
- When you claim "A is equivalent to B" or "X has been merged from Y",
|
|
87
|
+
compare their ACTUAL implementation, not just their names or descriptions
|
|
88
|
+
- If you haven't read both implementations, say "appears similar at the
|
|
89
|
+
description level; implementation equivalence not verified"
|
|
90
|
+
|
|
91
|
+
### 3. Counter-Example Search
|
|
92
|
+
- For each major conclusion, find at least 1 counter-example or edge case
|
|
93
|
+
- If you cannot find one, state that explicitly: "No counter-example found
|
|
94
|
+
within the examined scope"
|
|
95
|
+
- When comparing two systems, ask: "What does X do that Y CANNOT do?"
|
|
96
|
+
(and vice versa) — don't just list overlaps
|
|
97
|
+
|
|
98
|
+
### 4. Confidence Calibration
|
|
99
|
+
- Label each conclusion with confidence: [高] [中] [低]
|
|
100
|
+
- [高] = verified from source code or primary documentation
|
|
101
|
+
- [中] = inferred from description but not implementation-verified
|
|
102
|
+
- [低] = speculative, based on naming convention or surface similarity
|
|
103
|
+
|
|
104
|
+
### 5. Depth Check
|
|
105
|
+
- If your analysis is based ONLY on file names and description fields,
|
|
106
|
+
you are doing surface analysis — state this limitation upfront
|
|
107
|
+
- To reach depth: read at least one implementation file per comparison target
|
|
108
|
+
- Ask: "What would a domain expert notice that I'm missing?"
|
|
109
|
+
|
|
110
|
+
These checks are not optional for analysis tasks. Apply them before
|
|
111
|
+
presenting conclusions, and surface any [低] confidence findings
|
|
112
|
+
explicitly rather than burying them.`)
|
|
113
|
+
|
|
69
114
|
// Inject workflow auto-generation guidance
|
|
70
115
|
parts.push(`## Workflow Auto-Generation
|
|
71
116
|
|
|
@@ -102,6 +147,30 @@ Script format: export const meta = { name, description, phases: [...] }
|
|
|
102
147
|
this.skillsReminder = reminder
|
|
103
148
|
}
|
|
104
149
|
|
|
150
|
+
/**
|
|
151
|
+
* P2-2: Build a permission-mode context block for the system prompt.
|
|
152
|
+
* Tells the model its current permission level and what to expect.
|
|
153
|
+
*/
|
|
154
|
+
private buildPermissionContext(mode: string): string {
|
|
155
|
+
const modeDescriptions: Record<string, string> = {
|
|
156
|
+
default:
|
|
157
|
+
'You are in **default** mode. Tools marked as requiring approval will be blocked. Use Read/Grep/Glob for exploration.',
|
|
158
|
+
acceptEdits:
|
|
159
|
+
'You are in **acceptEdits** mode. File reads and edits are allowed; Bash requires approval.',
|
|
160
|
+
plan: 'You are in **plan** mode. Only Read/Grep/Glob are allowed — no file modifications or command execution.',
|
|
161
|
+
auto: 'You are in **auto** mode. Most tools run without approval. If a tool is blocked by security policy or hooks, try a different approach instead of retrying.',
|
|
162
|
+
dontAsk:
|
|
163
|
+
'You are in **dontAsk** mode. All tools blocked unless explicitly allowlisted. Check your allow rules before acting.',
|
|
164
|
+
bypassPermissions:
|
|
165
|
+
'You are in **bypassPermissions** mode. All tools are allowed. Use this power responsibly.',
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const description = modeDescriptions[mode]
|
|
169
|
+
if (!description) return ''
|
|
170
|
+
|
|
171
|
+
return `## Permission Context\n\n${description}\n\nWhen a tool is denied, do NOT retry with the same tool and similar parameters. Move on to a different approach or ask the user for guidance.`
|
|
172
|
+
}
|
|
173
|
+
|
|
105
174
|
list(): InstructionFile[] {
|
|
106
175
|
return [...this.instructions]
|
|
107
176
|
}
|
package/src/core/permission.ts
CHANGED
|
@@ -29,6 +29,13 @@ export class PermissionSystem {
|
|
|
29
29
|
// ── Org-level restrictions (P0 security) ──
|
|
30
30
|
private restrictions: PermissionRestrictions | undefined = undefined
|
|
31
31
|
|
|
32
|
+
// ── P0-5: Sub-agent mode (auto mode hardening for background tasks) ──
|
|
33
|
+
private isSubAgent = false
|
|
34
|
+
|
|
35
|
+
// ── P1-4: Consecutive block counter (prevents infinite retry loops) ──
|
|
36
|
+
private consecutiveBlockCount = 0
|
|
37
|
+
private static readonly MAX_CONSECUTIVE_BLOCKS = 3
|
|
38
|
+
|
|
32
39
|
/** Invalidate the permission cache (called on any rule/mode change). */
|
|
33
40
|
private invalidateCache(): void {
|
|
34
41
|
this.checkCache.clear()
|
|
@@ -77,6 +84,85 @@ export class PermissionSystem {
|
|
|
77
84
|
return this.restrictions
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
/**
|
|
88
|
+
* P0-5 (v2.1.222 alignment): Enable sub-agent safety mode.
|
|
89
|
+
* When true, auto mode returns 'ask' for Bash/Write/Edit tools instead of
|
|
90
|
+
* 'bypass', ensuring PreToolUse hooks remain the safety gate in background agents.
|
|
91
|
+
*/
|
|
92
|
+
setSubAgentMode(enabled: boolean): void {
|
|
93
|
+
this.isSubAgent = enabled
|
|
94
|
+
this.invalidateCache()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* P1-4 (v2.1.225 alignment): Increment the consecutive block counter
|
|
99
|
+
* when a tool is denied. Safety-filter refusals should NOT count.
|
|
100
|
+
* Returns true if the consecutive block limit has been exceeded.
|
|
101
|
+
*/
|
|
102
|
+
incrementBlockCounter(): boolean {
|
|
103
|
+
this.consecutiveBlockCount++
|
|
104
|
+
return this.consecutiveBlockCount >= PermissionSystem.MAX_CONSECUTIVE_BLOCKS
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Reset the block counter when a tool is successfully executed. */
|
|
108
|
+
resetBlockCounter(): void {
|
|
109
|
+
this.consecutiveBlockCount = 0
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Get the current consecutive block count. */
|
|
113
|
+
getBlockCount(): number {
|
|
114
|
+
return this.consecutiveBlockCount
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* P0-3 (v2.1.223 alignment): Create a permission context for a sub-agent.
|
|
119
|
+
*
|
|
120
|
+
* The sub-agent's requested permissionMode is clamped against the parent's
|
|
121
|
+
* org restrictions (maxAllowedMode, forbiddenModes). Deny rules from the
|
|
122
|
+
* parent are propagated so org safety policies always apply.
|
|
123
|
+
*
|
|
124
|
+
* Returns a new PermissionSystem instance — NOT shared with the parent.
|
|
125
|
+
*/
|
|
126
|
+
createSubAgentPermission(agentPermissionMode: string): PermissionSystem {
|
|
127
|
+
const resolvedMode = this.resolveAgentMode(agentPermissionMode)
|
|
128
|
+
const subPerm = new PermissionSystem(resolvedMode)
|
|
129
|
+
|
|
130
|
+
// Propagate org restrictions to sub-agent
|
|
131
|
+
if (this.restrictions) {
|
|
132
|
+
subPerm.setRestrictions(this.restrictions)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Propagate deny rules (org safety policies must always apply)
|
|
136
|
+
for (const denyEntry of this.denyRules) {
|
|
137
|
+
subPerm.deny(denyEntry.pattern)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return subPerm
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolve an agent's permissionMode string to a clamped PermissionMode.
|
|
145
|
+
* 'inherit' means "use the parent's current mode".
|
|
146
|
+
* 'bypass' is treated as an alias for 'bypassPermissions'.
|
|
147
|
+
*/
|
|
148
|
+
private resolveAgentMode(agentMode: string): PermissionMode {
|
|
149
|
+
// Normalize aliases
|
|
150
|
+
const normalized = agentMode === 'bypass' ? 'bypassPermissions' : agentMode
|
|
151
|
+
|
|
152
|
+
const modeMap: Record<string, PermissionMode> = {
|
|
153
|
+
bypassPermissions: 'bypassPermissions',
|
|
154
|
+
dontAsk: 'dontAsk',
|
|
155
|
+
auto: 'auto',
|
|
156
|
+
plan: 'plan',
|
|
157
|
+
acceptEdits: 'acceptEdits',
|
|
158
|
+
default: 'default',
|
|
159
|
+
inherit: this.mode,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const desired: PermissionMode = modeMap[normalized] || 'default'
|
|
163
|
+
return clampMode(desired, this.restrictions)
|
|
164
|
+
}
|
|
165
|
+
|
|
80
166
|
// ── Rule management ──
|
|
81
167
|
|
|
82
168
|
allow(rule: string): void {
|
|
@@ -267,6 +353,12 @@ export class PermissionSystem {
|
|
|
267
353
|
if (tool.name === 'SendMessage') {
|
|
268
354
|
return 'mode-baseline'
|
|
269
355
|
}
|
|
356
|
+
// P0-5: In sub-agent context, enforce 'ask' for destructive tools
|
|
357
|
+
// so hooks remain the sole safety gate. Without hooks, these tools
|
|
358
|
+
// would otherwise run completely un-gated in auto mode.
|
|
359
|
+
if (this.isSubAgent && ['Bash', 'Write', 'Edit'].includes(tool.name)) {
|
|
360
|
+
return 'ask'
|
|
361
|
+
}
|
|
270
362
|
return 'bypass'
|
|
271
363
|
|
|
272
364
|
case 'dontAsk':
|
package/src/index.tsx
CHANGED
|
@@ -238,12 +238,12 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
238
238
|
for (const msg of saved.messages) {
|
|
239
239
|
context.addMessage(msg)
|
|
240
240
|
}
|
|
241
|
-
context.setSystemPrompt(instructions.buildSystemPrompt())
|
|
241
|
+
context.setSystemPrompt(instructions.buildSystemPrompt(config.permission as string))
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
if (context.getMessageCount() === 0) {
|
|
246
|
-
const basePrompt = instructions.buildSystemPrompt()
|
|
246
|
+
const basePrompt = instructions.buildSystemPrompt(config.permission as string)
|
|
247
247
|
const memoryReminder = loadSessionMemories(basePrompt)
|
|
248
248
|
|
|
249
249
|
// Inject previous session summary for AI continuity
|
|
@@ -430,12 +430,19 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
430
430
|
const crossSessionConfig = loadCrossSessionConfig(process.cwd())
|
|
431
431
|
engine.setCrossSessionConfig(crossSessionConfig)
|
|
432
432
|
|
|
433
|
+
// P2-1: Trigger SessionStart hooks after full initialization
|
|
434
|
+
hookEngine.executeSessionStart(sessionName).catch(() => {
|
|
435
|
+
// Hook failures never block session startup
|
|
436
|
+
})
|
|
437
|
+
|
|
433
438
|
// Auto-save session on exit
|
|
434
439
|
let saved = false
|
|
435
440
|
const saveAndExit = () => {
|
|
436
441
|
saved = true
|
|
437
442
|
clearInterval(heartbeatInterval)
|
|
438
443
|
unregisterSession(sessionName)
|
|
444
|
+
// P2-1: Trigger SessionEnd hooks before cleanup (best-effort)
|
|
445
|
+
hookEngine.executeSessionEnd(sessionName).catch(() => {})
|
|
439
446
|
artifactServer.stop()
|
|
440
447
|
if (context.getMessageCount() > 0) {
|
|
441
448
|
SessionStore.save(sessionName, context.getMessages(), {
|