@aipper/aiws-spec 0.0.49 → 0.0.51
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 +3 -0
- package/docs/opencode-subagent-first.md +6 -0
- package/docs/pi-plugins-manifest.json +136 -0
- package/docs/pi-plugins-manifest.md +111 -0
- package/docs/pi-subagent-first.md +182 -0
- package/docs/workflow-delegation-context-injection.md +40 -3
- package/package.json +1 -1
- package/templates/workspace/.opencode/lib/aiws-context.js +225 -13
- package/templates/workspace/.opencode/plugins/aiws-inject-context.js +14 -2
- package/templates/workspace/.opencode/plugins/aiws-workflow-state.js +16 -1
package/README.md
CHANGED
|
@@ -32,5 +32,8 @@ npx @aipper/aiws init .
|
|
|
32
32
|
- `docs/workflow-delegation-contracts.schema.json`:delegation contract JSON 的结构约束
|
|
33
33
|
- `docs/opencode-omo-adapter.md`:OpenCode + oh-my-opencode 的适配边界、角色映射与 fallback 约定
|
|
34
34
|
- `docs/opencode-omo-validation-checklist.md`:OpenCode + oMo 的真实项目验收清单(探测、启用、损坏配置、fallback)
|
|
35
|
+
- `docs/opencode-subagent-first.md`:OpenCode 宿主 subagent-first 执行契约(主会话协调、worker/reviewer、hatch)
|
|
36
|
+
- `docs/pi-subagent-first.md`:Pi 宿主 subagent-first 执行契约(借力 `@tintinweb/pi-subagents`;AIWS force policy)
|
|
37
|
+
- `docs/pi-plugins-manifest.md` / `docs/pi-plugins-manifest.json`:Pi 用户级插件清单(required / recommended / forbidden)与 `aiws doctor` 检测约定
|
|
35
38
|
- `templates/workspace/.opencode/oh-my-opencode.json.example`:保守的项目级 oMo agent 建议配置(只覆盖 agents)
|
|
36
39
|
- `docs/collaboration-artifacts.md`:`analysis/patches/review/evidence` 的协同工件目录与生命周期约定
|
|
@@ -182,6 +182,12 @@ worker 返回 BLOCKED ──────────────→ [stop: 输
|
|
|
182
182
|
- `workflow-delegation-contracts.md` — 角色/scope/artifact 契约
|
|
183
183
|
- `workflow-delegation-context-injection.md` — JSONL curation 规范
|
|
184
184
|
|
|
185
|
+
## 跨宿主对照
|
|
186
|
+
|
|
187
|
+
- **Pi** 对等契约:`pi-subagent-first.md`(运行时借力 `@tintinweb/pi-subagents`;force policy 在 `.pi/extensions/aiws.ts` + `packages/aiws/src/lib/pi-force-policy.ts`)。
|
|
188
|
+
- Hatch 短语与四状态协议与 Pi 侧一致;派发工具不同(OpenCode/oMo `task` vs Pi `Agent`)。
|
|
189
|
+
|
|
185
190
|
## 更新历史
|
|
186
191
|
|
|
187
192
|
- v1 (2026-05-16): 初版。基于 trellis workflow-state 纪律 + superpowers subagent-driven-development 模式。
|
|
193
|
+
- v1.1 (2026-07-23): 增加 Pi 侧 `pi-subagent-first.md` 交叉链接。
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"scope": "user",
|
|
4
|
+
"source_protocol": "npm:",
|
|
5
|
+
"install_template": "pi install npm:{package}{versionSuffix}",
|
|
6
|
+
"required": [
|
|
7
|
+
{
|
|
8
|
+
"id": "@tintinweb/pi-subagents",
|
|
9
|
+
"package": "@tintinweb/pi-subagents",
|
|
10
|
+
"version": "0.14.2",
|
|
11
|
+
"tier": "required",
|
|
12
|
+
"reason": "Single approved Pi subagent runtime for AIWS force policy"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"recommended_a": [
|
|
16
|
+
{
|
|
17
|
+
"id": "pi-extension-settings",
|
|
18
|
+
"package": "pi-extension-settings",
|
|
19
|
+
"tier": "recommended_a"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "pi-powerbar",
|
|
23
|
+
"package": "pi-powerbar",
|
|
24
|
+
"tier": "recommended_a"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "pi-lsp",
|
|
28
|
+
"package": "pi-lsp",
|
|
29
|
+
"tier": "recommended_a"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "pi-hashline-edit-pro",
|
|
33
|
+
"package": "pi-hashline-edit-pro",
|
|
34
|
+
"tier": "recommended_a"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "rpiv-todo",
|
|
38
|
+
"package": "rpiv-todo",
|
|
39
|
+
"tier": "recommended_a"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "rpiv-ask-user-question",
|
|
43
|
+
"package": "rpiv-ask-user-question",
|
|
44
|
+
"tier": "recommended_a"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "pi-btw",
|
|
48
|
+
"package": "pi-btw",
|
|
49
|
+
"tier": "recommended_a"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "pi-caffeinate",
|
|
53
|
+
"package": "pi-caffeinate",
|
|
54
|
+
"tier": "recommended_a"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "pi-workspace-history",
|
|
58
|
+
"package": "pi-workspace-history",
|
|
59
|
+
"tier": "recommended_a"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"recommended_b": [
|
|
63
|
+
{
|
|
64
|
+
"id": "pi-mcp-adapter",
|
|
65
|
+
"package": "pi-mcp-adapter",
|
|
66
|
+
"tier": "recommended_b"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "pi-fff",
|
|
70
|
+
"package": "pi-fff",
|
|
71
|
+
"tier": "recommended_b"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "pi-slopchop",
|
|
75
|
+
"package": "pi-slopchop",
|
|
76
|
+
"tier": "recommended_b"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "plan-mode",
|
|
80
|
+
"package": "plan-mode",
|
|
81
|
+
"tier": "recommended_b"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "pi-cache-optimizer",
|
|
85
|
+
"package": "pi-cache-optimizer",
|
|
86
|
+
"tier": "recommended_b"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "pi-rtk-optimizer",
|
|
90
|
+
"package": "pi-rtk-optimizer",
|
|
91
|
+
"tier": "recommended_b"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "pi-agent-browser-native",
|
|
95
|
+
"package": "pi-agent-browser-native",
|
|
96
|
+
"tier": "recommended_b"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "pi-add-dir",
|
|
100
|
+
"package": "pi-add-dir",
|
|
101
|
+
"tier": "recommended_b"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": "pi-observational-memory",
|
|
105
|
+
"package": "pi-observational-memory",
|
|
106
|
+
"tier": "recommended_b",
|
|
107
|
+
"reason": "Long-session ledger compaction (observer/reflector); optional; exclusive vs other OM forks"
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"forbidden": [
|
|
111
|
+
{
|
|
112
|
+
"id": "@narumitw/pi-subagents",
|
|
113
|
+
"package": "@narumitw/pi-subagents",
|
|
114
|
+
"severity": "conflict",
|
|
115
|
+
"reason": "Second subagent runtime; exclusive with @tintinweb/pi-subagents"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "pi-subagents",
|
|
119
|
+
"package": "pi-subagents",
|
|
120
|
+
"severity": "conflict",
|
|
121
|
+
"reason": "Unscoped/third-party subagents package; exclusive with @tintinweb/pi-subagents"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"id": "@narumitw/pi-goal",
|
|
125
|
+
"package": "@narumitw/pi-goal",
|
|
126
|
+
"severity": "forbidden",
|
|
127
|
+
"reason": "Dual goal orchestrator vs aiws goal"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": "pi-autoresearch",
|
|
131
|
+
"package": "pi-autoresearch",
|
|
132
|
+
"severity": "forbidden",
|
|
133
|
+
"reason": "Long main-write research loops under AIWS governance"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Pi plugins manifest (AIWS)
|
|
2
|
+
|
|
3
|
+
**Change / Problem**: pi-plugin-doctor / PB-010
|
|
4
|
+
**Req**: TOOLING-002L
|
|
5
|
+
**Scope**: **user-level** Pi packages (`pi install npm:…` → typically `~/.pi/agent/`)
|
|
6
|
+
**Non-goals**: silent auto-install; project-local plugin install as default; dual subagent runtimes; reimplement spawn/UI
|
|
7
|
+
|
|
8
|
+
AIWS owns **rules + detection + install prompts**. Runtime spawn remains `@tintinweb/pi-subagents`.
|
|
9
|
+
|
|
10
|
+
Machine-readable twin: `pi-plugins-manifest.json` (same directory).
|
|
11
|
+
|
|
12
|
+
## Install conventions
|
|
13
|
+
|
|
14
|
+
| Item | Rule |
|
|
15
|
+
|------|------|
|
|
16
|
+
| Source protocol | `npm:` only (no `pnpm:` source protocol) |
|
|
17
|
+
| Backend package manager | optional `settings.npmCommand: ["pnpm"]` (or npm) |
|
|
18
|
+
| Default command | `pi install npm:<package>[@version]` (user scope; do **not** use project `-l` for these packages) |
|
|
19
|
+
| Project `.pi/` | governance only: agents, extensions, skills, `subagents.json` |
|
|
20
|
+
|
|
21
|
+
## Prerequisite: Pi agent CLI
|
|
22
|
+
|
|
23
|
+
`--install-pi-plugins` **requires** global Pi CLI (`pi` on PATH), typically:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm i -g @mariozechner/pi-coding-agent
|
|
27
|
+
# or
|
|
28
|
+
pnpm add -g @mariozechner/pi-coding-agent
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
AIWS **detects** missing `pi` and prints these commands; it does **not** auto-run global install.
|
|
32
|
+
If `pi` is missing, `aiws doctor|init|update --install-pi-plugins` exits non-zero (unless warn-only) and does not call `pi install`.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## Layers
|
|
36
|
+
|
|
37
|
+
### required
|
|
38
|
+
|
|
39
|
+
Must be present for AIWS Pi force / subagent-first.
|
|
40
|
+
|
|
41
|
+
| package | pin | reason |
|
|
42
|
+
|---------|-----|--------|
|
|
43
|
+
| `@tintinweb/pi-subagents` | `0.14.2` | Single approved subagent runtime (Agent / get_subagent_result / steer_subagent); aligns with `pi-subagent-first.md` |
|
|
44
|
+
|
|
45
|
+
Install:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pi install npm:@tintinweb/pi-subagents@0.14.2
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### recommended (tier A — safe defaults)
|
|
52
|
+
|
|
53
|
+
Low conflict with AIWS governance; useful quality-of-life.
|
|
54
|
+
|
|
55
|
+
| package | notes |
|
|
56
|
+
|---------|--------|
|
|
57
|
+
| `pi-extension-settings` | settings UX |
|
|
58
|
+
| `pi-powerbar` | status bar |
|
|
59
|
+
| `pi-lsp` | LSP |
|
|
60
|
+
| `pi-hashline-edit-pro` | edit helper (watch tool names vs force deny) |
|
|
61
|
+
| `rpiv-todo` | todos |
|
|
62
|
+
| `rpiv-ask-user-question` | structured questions |
|
|
63
|
+
| `pi-btw` | asides |
|
|
64
|
+
| `pi-caffeinate` | keep awake |
|
|
65
|
+
| `pi-workspace-history` | history |
|
|
66
|
+
|
|
67
|
+
### recommended (tier B — on demand)
|
|
68
|
+
|
|
69
|
+
| package | notes |
|
|
70
|
+
|---------|--------|
|
|
71
|
+
| `pi-mcp-adapter` | MCP |
|
|
72
|
+
| `pi-fff` | file finder |
|
|
73
|
+
| `pi-slopchop` | slop cleanup |
|
|
74
|
+
| `plan-mode` | plan UX (AIWS keeps plan truth) |
|
|
75
|
+
| `pi-cache-optimizer` | cache (truncation caution) |
|
|
76
|
+
| `pi-rtk-optimizer` | RTK |
|
|
77
|
+
| `pi-agent-browser-native` | browser |
|
|
78
|
+
| `pi-observational-memory` | Long-session OM (elpapi42 V3); optional; do not dual-install other OM forks |
|
|
79
|
+
| `pi-add-dir` | add dirs |
|
|
80
|
+
|
|
81
|
+
### forbidden / conflict
|
|
82
|
+
|
|
83
|
+
| package / pattern | severity | reason |
|
|
84
|
+
|-------------------|----------|--------|
|
|
85
|
+
| `@narumitw/pi-subagents` | conflict | Second subagent runtime; not API-compatible with tintinweb mapping |
|
|
86
|
+
| `pi-subagents` (unscoped / nicobailon-style) | conflict | Third ecosystem variant; exclusive with tintinweb |
|
|
87
|
+
| `@narumitw/pi-goal` | forbidden | Dual goal orchestrator vs `aiws goal` |
|
|
88
|
+
| `pi-autoresearch` | forbidden | Long main-write research loops under AIWS governance |
|
|
89
|
+
|
|
90
|
+
**Rule**: install **exactly one** subagent runtime — AIWS requires `@tintinweb/pi-subagents` only.
|
|
91
|
+
|
|
92
|
+
## Detect sources (priority)
|
|
93
|
+
|
|
94
|
+
1. `pi list` if `pi` is on PATH
|
|
95
|
+
2. `~/.pi/agent/settings.json` and/or packages metadata under `~/.pi/agent/`
|
|
96
|
+
3. If neither available → report `pi_cli_missing` + install hints; do not crash
|
|
97
|
+
|
|
98
|
+
## CLI behavior (`aiws doctor`)
|
|
99
|
+
|
|
100
|
+
- Print layered report + copy-paste `pi install npm:…` lines
|
|
101
|
+
- **Default**: no install
|
|
102
|
+
- Missing **required** → exit code ≠ 0
|
|
103
|
+
- Conflicts / forbidden installed → warn (exit ≠ 0 if any required missing or any conflict)
|
|
104
|
+
- `init --pi` / `update` (when `.pi/` exists or `--pi`): print same report as **warn**; do not fail init/update by default
|
|
105
|
+
- Optional `--install-pi-plugins`: install **required only** (user scope); recommended stay print-only
|
|
106
|
+
|
|
107
|
+
## Related
|
|
108
|
+
|
|
109
|
+
- `docs/pi-subagent-first.md`
|
|
110
|
+
- `packages/aiws/src/lib/pi-plugins-doctor.ts`
|
|
111
|
+
- `packages/aiws/src/lib/pi-force-policy.ts`
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Pi Subagent-First Execution
|
|
2
|
+
|
|
3
|
+
<!-- Managed by aiws. Do not hand edit. -->
|
|
4
|
+
|
|
5
|
+
**规范 ID**: `pi-subagent-first`
|
|
6
|
+
**版本**: 1
|
|
7
|
+
**运行时**: `@tintinweb/pi-subagents` ≥ 0.14.2(spawn/UI/RPC)
|
|
8
|
+
**策略归属**: AIWS(`.pi/extensions/aiws.ts` + validate + 本规范)
|
|
9
|
+
|
|
10
|
+
## 目的
|
|
11
|
+
|
|
12
|
+
在 **Pi** 宿主中强制 subagent-first:主 session 只做协调/收敛;实现与审查由 `aiws-worker` / `aiws-reviewer` 完成。对齐 `opencode-subagent-first.md` 的执行纪律,堵住主 session 直接 `write`/`edit` 业务代码的失败模式。
|
|
13
|
+
|
|
14
|
+
**非目标**
|
|
15
|
+
|
|
16
|
+
- 不自研 subagent runtime / pane daemon / 第二 orchestrator(TOOLING-002L)。
|
|
17
|
+
- **不修改 OpenCode 插件 write-deny**(另 change)。
|
|
18
|
+
- 不与 Trellis `trellis_subagent` 双 runtime;Pi 侧仅 `pi-subagents`。
|
|
19
|
+
|
|
20
|
+
## 核心原则
|
|
21
|
+
|
|
22
|
+
1. **Subagent-first**: 实现默认 `Agent({ subagent_type: "aiws-worker", ... })`;审查默认 `aiws-reviewer`。
|
|
23
|
+
2. **Review loop**: 每个 worker 交付后至少一次 reviewer(或等价 `ws-quality-review`);fail 则修再审。
|
|
24
|
+
3. **Continuous**: 主 session 不因“下一步做什么”主动停顿——除非 BLOCKED / NEEDS_CONTEXT 超限 / 任务完成。
|
|
25
|
+
4. **Explicit override (hatch)**: 仅用户**当前消息**显式短语可允许主 session 写实现;必须落盘 hatch 证据。
|
|
26
|
+
5. **No silent fallback**: 委托失败 → 写 `evidence/delegate-failure.md` 并 **stop**;禁止默默改由主 session 写业务代码。
|
|
27
|
+
6. **Type whitelist**: 实现路径仅允许 `aiws-worker` | `aiws-reviewer`。pi-subagents 对未知 type 会 **fallback 到 general-purpose**——AIWS 扩展必须拒绝非白名单 type。
|
|
28
|
+
|
|
29
|
+
## 运行时安装
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pi install npm:@tintinweb/pi-subagents
|
|
33
|
+
# 建议钉版本: @tintinweb/pi-subagents@0.14.2
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
父 session 可用工具(由 pi-subagents 提供):
|
|
37
|
+
|
|
38
|
+
| Tool | 用途 |
|
|
39
|
+
|------|------|
|
|
40
|
+
| `Agent` | 派生子 agent |
|
|
41
|
+
| `get_subagent_result` | 取回完成结果 |
|
|
42
|
+
| `steer_subagent` | 运行中纠偏 |
|
|
43
|
+
|
|
44
|
+
项目配置建议 `.pi/subagents.json`:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"disableDefaultAgents": true
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`disableDefaultAgents: true` 关闭内置 `general-purpose` / `Explore` / `Plan` 的默认注册,减少“随便派 general-purpose 写代码”的逃逸。自定义 `.pi/agents/aiws-*.md` 仍可用。
|
|
53
|
+
|
|
54
|
+
自定义 agent 发现顺序(高优先):
|
|
55
|
+
|
|
56
|
+
1. `.pi/agents/<name>.md`
|
|
57
|
+
2. `.agents/agents/<name>.md`
|
|
58
|
+
3. `~/.pi/agent/agents/<name>.md`
|
|
59
|
+
|
|
60
|
+
## 角色
|
|
61
|
+
|
|
62
|
+
| 角色 | type | 工具意图 |
|
|
63
|
+
|------|------|----------|
|
|
64
|
+
| 主 session / orchestrator | 父会话 | 只读 + `Agent` / `get_subagent_result` / `steer_subagent` + `aiws` CLI;**无 hatch 时禁止业务路径 write/edit** |
|
|
65
|
+
| 实现者 | `aiws-worker` | read, write, edit, bash, find, grep;禁止 git commit/push/merge |
|
|
66
|
+
| 审查者 | `aiws-reviewer` | 只读为主;审查产物写 `.aiws/changes/**/review|evidence`(由策略允许) |
|
|
67
|
+
|
|
68
|
+
**已弃用主路径**: `aiws_subagent`(`ctx.newSession`)。可保留兼容警告/转发,不得作为 primary。
|
|
69
|
+
|
|
70
|
+
## 执行循环(Continuous Loop)
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
主 session 策划上下文(JSONL / prompt 边界)
|
|
74
|
+
→ Agent(subagent_type=aiws-worker, prompt=…)
|
|
75
|
+
→ worker: DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED
|
|
76
|
+
→ DONE → Agent(subagent_type=aiws-reviewer, …)
|
|
77
|
+
→ reviewer pass → 收敛 evidence → 准备 finish
|
|
78
|
+
→ reviewer fail → worker 修复(≤3 轮)
|
|
79
|
+
→ DONE_WITH_CONCERNS → quality 评估残余风险
|
|
80
|
+
→ NEEDS_CONTEXT → 补上下文重试(≤2)
|
|
81
|
+
→ BLOCKED → 停止并输出 blocker
|
|
82
|
+
→ 委托失败/超时 → evidence/delegate-failure.md + stop(禁止静默主写)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Worker 报告格式
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
**Status:** DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED
|
|
89
|
+
**Completed:** 实现内容摘要
|
|
90
|
+
**Files Changed:** 文件清单
|
|
91
|
+
**Verification:** 命令 + 结果
|
|
92
|
+
**Artifacts:** changes/<id>/analysis|patches|evidence 路径
|
|
93
|
+
**Concerns:** 疑虑(若有)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Reviewer
|
|
97
|
+
|
|
98
|
+
- 输入:patches / evidence / analysis,**不是**主 session 完整历史。
|
|
99
|
+
- 输出:pass/fail + findings;落盘 `changes/<id>/review/`。
|
|
100
|
+
|
|
101
|
+
### 停止条件
|
|
102
|
+
|
|
103
|
+
- BLOCKED(治理/脏树/validate 失败等)
|
|
104
|
+
- NEEDS_CONTEXT 超过 2 次
|
|
105
|
+
- 验证缺失或失败
|
|
106
|
+
- 需 host permission
|
|
107
|
+
- 用户改 scope
|
|
108
|
+
- 委托失败已记录且未恢复
|
|
109
|
+
- 全部任务完成且 evidence 收敛
|
|
110
|
+
|
|
111
|
+
## Inline Escape Hatch
|
|
112
|
+
|
|
113
|
+
**当前用户消息**含以下之一时,主 session 可直接写实现:
|
|
114
|
+
|
|
115
|
+
- `直接改` / `do it inline` / `你直接改`
|
|
116
|
+
- `别派 sub-agent` / `main session 写就行`
|
|
117
|
+
- `不用 sub-agent` / `no sub-agent`
|
|
118
|
+
|
|
119
|
+
规则:
|
|
120
|
+
|
|
121
|
+
- 仅**当前**消息;不得沿用上轮
|
|
122
|
+
- 主 session 不得自行推断
|
|
123
|
+
- 必须写 `changes/<id>/evidence/inline-escape-hatch.md`
|
|
124
|
+
- hatch 有效期内仍应优先小步、可验证
|
|
125
|
+
|
|
126
|
+
## 主会话写路径策略(L1)
|
|
127
|
+
|
|
128
|
+
**无 hatch 时**:
|
|
129
|
+
|
|
130
|
+
- **拒绝** 对业务代码路径的 write/edit(扩展拦截或等价门禁)
|
|
131
|
+
- **允许**(白名单示例):
|
|
132
|
+
- `.aiws/changes/**/evidence/**`
|
|
133
|
+
- `.aiws/changes/**/analysis/**`
|
|
134
|
+
- `.aiws/changes/**/review/**`
|
|
135
|
+
- `.aiws/plan/**`
|
|
136
|
+
- (可选)`.aiws/goals/**`
|
|
137
|
+
|
|
138
|
+
**Agent type**:仅 `aiws-worker` | `aiws-reviewer`;其它 type → 拒绝并要求改 type 或 hatch。
|
|
139
|
+
|
|
140
|
+
**Delegate 失败**:写 `evidence/delegate-failure.md`,停止;禁止无记录地改由主 session 实现业务逻辑。
|
|
141
|
+
|
|
142
|
+
## Context 纪律
|
|
143
|
+
|
|
144
|
+
与 OpenCode 侧相同精神:
|
|
145
|
+
|
|
146
|
+
1. 从委托合同整理 JSONL / 明确路径列表
|
|
147
|
+
2. high+medium 文件数与行数预算
|
|
148
|
+
3. 写入 `changes/<id>/analysis/<role>-context.jsonl`(若使用)
|
|
149
|
+
4. Reviewer 只收工作产物
|
|
150
|
+
|
|
151
|
+
详见 `workflow-delegation-context-injection.md`、`workflow-delegation-contracts.md`。
|
|
152
|
+
|
|
153
|
+
## 四状态协议
|
|
154
|
+
|
|
155
|
+
| 状态 | 含义 | 后续 |
|
|
156
|
+
|------|------|------|
|
|
157
|
+
| DONE | 完成且验证通过 | → reviewer |
|
|
158
|
+
| DONE_WITH_CONCERNS | 完成但有残余风险 | → quality 评估 |
|
|
159
|
+
| NEEDS_CONTEXT | 缺上下文 | 补后重试 ≤2;仍失败 → 记 failure,**不**静默主写 |
|
|
160
|
+
| BLOCKED | 治理阻断 | stop + blocker |
|
|
161
|
+
|
|
162
|
+
## 与 OpenCode 对照
|
|
163
|
+
|
|
164
|
+
| 项 | OpenCode (`opencode-subagent-first`) | Pi(本规范) |
|
|
165
|
+
|----|-------------------------------------|--------------|
|
|
166
|
+
| 派发 | oMo / task 等 | pi-subagents `Agent` |
|
|
167
|
+
| 实现 type | aiws-worker | aiws-worker(`.pi/agents/`) |
|
|
168
|
+
| 审查 type | aiws-reviewer | aiws-reviewer |
|
|
169
|
+
| Hatch | 相同短语 | 相同短语 |
|
|
170
|
+
| 策略硬门 | 插件/约定 | `.pi/extensions/aiws.ts` + validate |
|
|
171
|
+
| 未知 type | 平台相关 | 必须拒(因 general-purpose fallback) |
|
|
172
|
+
|
|
173
|
+
## 引用
|
|
174
|
+
|
|
175
|
+
- `opencode-subagent-first.md` — OpenCode 侧对等契约
|
|
176
|
+
- `.aiws/changes/*/analysis/pi-subagents-api-notes.md` — 版本化 API 笔记(变更工件)
|
|
177
|
+
- `workflow-delegation-contracts.md`
|
|
178
|
+
- `AI_PROJECT.md` Pi 相关规则(项目真值)
|
|
179
|
+
|
|
180
|
+
## 更新历史
|
|
181
|
+
|
|
182
|
+
- v1 (2026-07-23): 初版。借力 pi-subagents 0.14.x;AIWS 拥有 force policy(deny / whitelist / hatch / no silent fallback)。
|
|
@@ -73,6 +73,10 @@ changes/<id>/analysis/<role>-context.jsonl
|
|
|
73
73
|
|
|
74
74
|
aiws 提供了两个 OpenCode 插件,自动实现上下文注入:
|
|
75
75
|
|
|
76
|
+
### aiws-workflow-state(chat.message hook,每轮)
|
|
77
|
+
|
|
78
|
+
每轮用户消息注入 `<workflow-state>` breadcrumb(active change / phase)。支持 skip keyword 默认 `no-aiws`(见 §per-turn workflow-state skip)。
|
|
79
|
+
|
|
76
80
|
### aiws-session-start(chat.message hook)
|
|
77
81
|
|
|
78
82
|
每个新会话启动时,自动读取当前 change 上下文、项目真值、规范索引,并注入到第一条用户消息中。
|
|
@@ -90,8 +94,8 @@ aiws 提供了两个 OpenCode 插件,自动实现上下文注入:
|
|
|
90
94
|
当 `task()` 调用且 prompt 中包含 `role: <role>` 标记时自动拦截,读取 `changes/<id>/analysis/<role>-context.jsonl` 并注入到子 agent prompt。
|
|
91
95
|
|
|
92
96
|
**自动注入内容**:
|
|
93
|
-
1. JSONL 中引用的每个文件内容(按 priority
|
|
94
|
-
2. `<aiws-context-
|
|
97
|
+
1. JSONL 中引用的每个文件内容(按 priority 排序;运行时字节 cap 后可能为 truncated/index 形态)
|
|
98
|
+
2. `<aiws-context-meta>` 头部(含 role、change、files 计数:inline/truncated/index)
|
|
95
99
|
3. 粘性指令:要求子 agent 先读上下文文件再工作
|
|
96
100
|
|
|
97
101
|
当插件被激活时,主 agent 不需要手动粘性指令模板——插件会自动生成。
|
|
@@ -151,7 +155,40 @@ aiws 提供了两个 OpenCode 插件,自动实现上下文注入:
|
|
|
151
155
|
|
|
152
156
|
## 上下文预算管理
|
|
153
157
|
|
|
154
|
-
子 agent
|
|
158
|
+
子 agent 的上下文窗口有限。策展者必须执行预算检查。除策展软规则外,OpenCode 插件还施加运行时字节 cap(见下)。
|
|
159
|
+
|
|
160
|
+
### 运行时字节 cap(trellis #441 / TOOLING-003A)
|
|
161
|
+
|
|
162
|
+
策展层的 5 文件 / 4000 行规则**不能**单独保证子 agent prompt 安全。OpenCode 插件在 `readJsonlWithFiles` 中额外执行运行时字节 cap(与 trellis `context_injection` 对齐):
|
|
163
|
+
|
|
164
|
+
| 限制 | 默认 | 环境变量 | `0` 含义 |
|
|
165
|
+
|------|------|----------|----------|
|
|
166
|
+
| 单文件 (`max_file_bytes`) | 32768 (32KiB) | `AIWS_CONTEXT_MAX_FILE_BYTES` | 不限 |
|
|
167
|
+
| 单 artifact | 65536 (64KiB) | `AIWS_CONTEXT_MAX_ARTIFACT_BYTES` | 不限(预留;当前 JSONL 文件路径共用 file cap) |
|
|
168
|
+
| 注入总量 | 131072 (128KiB) | `AIWS_CONTEXT_MAX_TOTAL_BYTES` | 不限 |
|
|
169
|
+
|
|
170
|
+
**处理管线(每条 JSONL 条目)**:
|
|
171
|
+
1. 读取文件字节 → 可选 `sections` 行裁剪(**先于**字节 cap)
|
|
172
|
+
2. 按 `priority` 排序(high → medium → low)
|
|
173
|
+
3. 单文件 `truncateUtf8`:不切断 UTF-8 多字节序列;截断时追加 notice:`[AIWS: truncated at N bytes — read <path> for the full content]`
|
|
174
|
+
4. 总量预算:若完整 `=== path ===` 块放不下,则 **degrade-to-index**,仅注入:`[AIWS: not inlined (total context limit reached) — path (size bytes): reason]`
|
|
175
|
+
5. 条目 `mode`:`inline` | `truncated` | `index`
|
|
176
|
+
|
|
177
|
+
**验证入口**:`node scripts/test-context-injection-limits.mjs`
|
|
178
|
+
|
|
179
|
+
### per-turn workflow-state skip(trellis #427 / TOOLING-003A)
|
|
180
|
+
|
|
181
|
+
`aiws-workflow-state` 在每条用户消息注入 breadcrumb。若用户 prompt 文本中出现 skip keyword(**词边界、大小写不敏感**;连字符算词字符),则本轮**不注入**。
|
|
182
|
+
|
|
183
|
+
| 项 | 值 |
|
|
184
|
+
|----|-----|
|
|
185
|
+
| 默认 keyword | `no-aiws` |
|
|
186
|
+
| 覆盖 | 环境变量 `AIWS_WORKFLOW_SKIP_KEYWORD` |
|
|
187
|
+
| 禁用 escape hatch | `AIWS_WORKFLOW_SKIP_KEYWORD=`(空串;keyword 永不匹配) |
|
|
188
|
+
|
|
189
|
+
匹配示例:`no-aiws how...` / `(no-aiws)` / `path/no-aiws.md` 会 skip;`no-aiwsx` / `foo-no-aiws` **不会**。
|
|
190
|
+
|
|
191
|
+
**验证入口**:`node scripts/test-workflow-skip-keyword.mjs`
|
|
155
192
|
|
|
156
193
|
### 预算规则
|
|
157
194
|
|
package/package.json
CHANGED
|
@@ -70,6 +70,133 @@ function parseSections(entry) {
|
|
|
70
70
|
}).filter(Boolean)
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
|
|
74
|
+
// ============================================================
|
|
75
|
+
// Context injection limits (trellis #441 / TOOLING-003A)
|
|
76
|
+
// Defaults mirror trellis: 32KiB / 64KiB / 128KiB; 0 = unlimited
|
|
77
|
+
// Env: AIWS_CONTEXT_MAX_FILE_BYTES | AIWS_CONTEXT_MAX_ARTIFACT_BYTES | AIWS_CONTEXT_MAX_TOTAL_BYTES
|
|
78
|
+
// ============================================================
|
|
79
|
+
|
|
80
|
+
export const DEFAULT_CONTEXT_INJECTION_LIMITS = Object.freeze({
|
|
81
|
+
max_file_bytes: 32768,
|
|
82
|
+
max_artifact_bytes: 65536,
|
|
83
|
+
max_total_bytes: 131072,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse a non-negative integer env override; invalid/missing → default.
|
|
88
|
+
* Note: empty string is invalid → default (use "0" for unlimited).
|
|
89
|
+
*/
|
|
90
|
+
function parseLimitEnv(name, defaultValue) {
|
|
91
|
+
const raw = process.env[name]
|
|
92
|
+
if (raw === undefined || raw === null || raw === "") return defaultValue
|
|
93
|
+
const n = Number.parseInt(String(raw), 10)
|
|
94
|
+
if (!Number.isFinite(n) || n < 0) return defaultValue
|
|
95
|
+
return n
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Resolve context injection byte limits from env (or defaults). */
|
|
99
|
+
export function getContextInjectionLimits() {
|
|
100
|
+
return {
|
|
101
|
+
max_file_bytes: parseLimitEnv("AIWS_CONTEXT_MAX_FILE_BYTES", DEFAULT_CONTEXT_INJECTION_LIMITS.max_file_bytes),
|
|
102
|
+
max_artifact_bytes: parseLimitEnv("AIWS_CONTEXT_MAX_ARTIFACT_BYTES", DEFAULT_CONTEXT_INJECTION_LIMITS.max_artifact_bytes),
|
|
103
|
+
max_total_bytes: parseLimitEnv("AIWS_CONTEXT_MAX_TOTAL_BYTES", DEFAULT_CONTEXT_INJECTION_LIMITS.max_total_bytes),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Truncate raw bytes to at most `cap` without splitting a UTF-8 multi-byte sequence.
|
|
109
|
+
* `cap <= 0` means unlimited — returns data unchanged.
|
|
110
|
+
* @param {Buffer} data
|
|
111
|
+
* @param {number} cap
|
|
112
|
+
* @returns {Buffer}
|
|
113
|
+
*/
|
|
114
|
+
export function truncateUtf8(data, cap) {
|
|
115
|
+
if (!Buffer.isBuffer(data)) {
|
|
116
|
+
data = Buffer.from(data ?? "", "utf8")
|
|
117
|
+
}
|
|
118
|
+
if (cap <= 0 || data.length <= cap) return data
|
|
119
|
+
|
|
120
|
+
let truncated = data.subarray(0, cap)
|
|
121
|
+
let i = truncated.length
|
|
122
|
+
// Back off over continuation bytes (10xxxxxx)
|
|
123
|
+
while (i > 0 && (truncated[i - 1] & 0xc0) === 0x80) {
|
|
124
|
+
i -= 1
|
|
125
|
+
}
|
|
126
|
+
if (i === 0) return Buffer.alloc(0)
|
|
127
|
+
|
|
128
|
+
const lead = truncated[i - 1]
|
|
129
|
+
if (lead & 0x80) {
|
|
130
|
+
let seqLen = 1
|
|
131
|
+
if ((lead & 0xe0) === 0xc0) seqLen = 2
|
|
132
|
+
else if ((lead & 0xf0) === 0xe0) seqLen = 3
|
|
133
|
+
else if ((lead & 0xf8) === 0xf0) seqLen = 4
|
|
134
|
+
// Drop the lead byte too if its full sequence didn't fit
|
|
135
|
+
if (i - 1 + seqLen > truncated.length) {
|
|
136
|
+
i -= 1
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return truncated.subarray(0, i)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function truncateNotice(path, cap) {
|
|
143
|
+
return `\n[AIWS: truncated at ${cap} bytes — read ${path} for the full content]`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function indexNotice(path, size, reason) {
|
|
147
|
+
return (
|
|
148
|
+
`[AIWS: not inlined (total context limit reached) — ` +
|
|
149
|
+
`${path} (${size} bytes): ${reason || "budget"}]`
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class ContextBudget {
|
|
154
|
+
constructor(maxTotalBytes) {
|
|
155
|
+
this.maxTotalBytes = maxTotalBytes
|
|
156
|
+
this.used = 0
|
|
157
|
+
}
|
|
158
|
+
hasRoom(size) {
|
|
159
|
+
if (this.maxTotalBytes <= 0) return true
|
|
160
|
+
return this.used + size <= this.maxTotalBytes
|
|
161
|
+
}
|
|
162
|
+
add(size) {
|
|
163
|
+
this.used += size
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================================
|
|
168
|
+
// Workflow skip keyword (trellis #427 / TOOLING-003A)
|
|
169
|
+
// Default: no-aiws; env AIWS_WORKFLOW_SKIP_KEYWORD ("" disables)
|
|
170
|
+
// ============================================================
|
|
171
|
+
|
|
172
|
+
export const DEFAULT_WORKFLOW_SKIP_KEYWORD = "no-aiws"
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolve per-turn workflow-state skip keyword.
|
|
176
|
+
* - unset → "no-aiws"
|
|
177
|
+
* - "" → disabled (never matches)
|
|
178
|
+
* - any other string → custom keyword
|
|
179
|
+
*/
|
|
180
|
+
export function resolveWorkflowSkipKeyword() {
|
|
181
|
+
if (!Object.prototype.hasOwnProperty.call(process.env, "AIWS_WORKFLOW_SKIP_KEYWORD")) {
|
|
182
|
+
return DEFAULT_WORKFLOW_SKIP_KEYWORD
|
|
183
|
+
}
|
|
184
|
+
return String(process.env.AIWS_WORKFLOW_SKIP_KEYWORD)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Case-insensitive, word-boundary match of `keyword` in `text`.
|
|
189
|
+
* Hyphen counts as a word char so "no-aiwsx" / "foo-no-aiws" don't match.
|
|
190
|
+
* Empty keyword never matches.
|
|
191
|
+
*/
|
|
192
|
+
export function promptHasSkipKeyword(text, keyword) {
|
|
193
|
+
if (!keyword || typeof text !== "string") return false
|
|
194
|
+
const escaped = String(keyword).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
195
|
+
const pattern = new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i")
|
|
196
|
+
return pattern.test(text)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
73
200
|
// ============================================================
|
|
74
201
|
// AiwsContext
|
|
75
202
|
// ============================================================
|
|
@@ -145,11 +272,28 @@ export class AiwsContext {
|
|
|
145
272
|
* Returns array of { path, content, priority, kind, reason }
|
|
146
273
|
* Sorted by priority: high → medium → low
|
|
147
274
|
*/
|
|
148
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Read a JSONL file and load referenced file contents with byte caps (#441).
|
|
277
|
+
*
|
|
278
|
+
* Supports aiws 5-field format:
|
|
279
|
+
* {"file"|"glob": "...", "sections": [[25,50]], "priority": "high", "kind": "truth", "reason": "why"}
|
|
280
|
+
*
|
|
281
|
+
* Pipeline per entry (after priority sort of *candidates*):
|
|
282
|
+
* 1. read file bytes
|
|
283
|
+
* 2. optional section filter (line-based on UTF-8 text)
|
|
284
|
+
* 3. per-file truncateUtf8 (max_file_bytes; 0 = unlimited)
|
|
285
|
+
* 4. total budget: if full block won't fit → mode "index" notice only
|
|
286
|
+
*
|
|
287
|
+
* Returns array of:
|
|
288
|
+
* { path, content, priority, kind, reason, mode: "inline"|"truncated"|"index", byteSize?, originalByteSize? }
|
|
289
|
+
* Sorted by priority: high → medium → low (stable relative to JSONL order within tier).
|
|
290
|
+
*/
|
|
291
|
+
readJsonlWithFiles(jsonlPath, limits = null) {
|
|
149
292
|
const content = this.readFile(jsonlPath)
|
|
150
293
|
if (!content) return []
|
|
151
294
|
|
|
152
|
-
const
|
|
295
|
+
const lim = limits || getContextInjectionLimits()
|
|
296
|
+
const candidates = []
|
|
153
297
|
|
|
154
298
|
for (const rawLine of content.split(/\r?\n/)) {
|
|
155
299
|
const line = rawLine.trim()
|
|
@@ -157,7 +301,6 @@ export class AiwsContext {
|
|
|
157
301
|
try {
|
|
158
302
|
const item = JSON.parse(line)
|
|
159
303
|
|
|
160
|
-
// Resolve glob to actual file
|
|
161
304
|
const globPattern = item.glob || item.file
|
|
162
305
|
if (!globPattern) continue
|
|
163
306
|
|
|
@@ -166,32 +309,98 @@ export class AiwsContext {
|
|
|
166
309
|
const reason = item.reason || ""
|
|
167
310
|
const sections = parseSections(item)
|
|
168
311
|
|
|
169
|
-
// Read the file
|
|
170
312
|
const fullPath = resolvePath(this.directory, globPattern)
|
|
171
|
-
let
|
|
172
|
-
|
|
313
|
+
let rawBytes
|
|
314
|
+
try {
|
|
315
|
+
if (!existsSync(fullPath)) continue
|
|
316
|
+
rawBytes = readFileSync(fullPath)
|
|
317
|
+
} catch {
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
if (!rawBytes || rawBytes.length === 0) {
|
|
321
|
+
// empty file still allowed as empty content
|
|
322
|
+
rawBytes = Buffer.alloc(0)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let text = rawBytes.toString("utf8")
|
|
326
|
+
const originalByteSize = Buffer.byteLength(text, "utf8")
|
|
173
327
|
|
|
174
|
-
// Apply section filtering if specified
|
|
328
|
+
// Apply section filtering if specified (before byte caps)
|
|
175
329
|
if (sections) {
|
|
176
|
-
const lines =
|
|
330
|
+
const lines = text.split("\n")
|
|
177
331
|
const filtered = []
|
|
178
332
|
for (const [start, end] of sections) {
|
|
179
333
|
const from = Math.max(0, start - 1)
|
|
180
334
|
const to = Math.min(lines.length, end)
|
|
181
335
|
filtered.push(...lines.slice(from, to))
|
|
182
336
|
}
|
|
183
|
-
|
|
337
|
+
text = filtered.join("\n")
|
|
184
338
|
}
|
|
185
339
|
|
|
186
|
-
|
|
340
|
+
candidates.push({
|
|
341
|
+
path: globPattern,
|
|
342
|
+
text,
|
|
343
|
+
priority,
|
|
344
|
+
kind,
|
|
345
|
+
reason,
|
|
346
|
+
originalByteSize,
|
|
347
|
+
})
|
|
187
348
|
} catch {
|
|
188
349
|
// Skip malformed lines
|
|
189
350
|
}
|
|
190
351
|
}
|
|
191
352
|
|
|
192
|
-
// Sort: high → medium → low
|
|
353
|
+
// Sort: high → medium → low (preserve relative order within tier via stable sort)
|
|
193
354
|
const order = { high: 0, medium: 1, low: 2 }
|
|
194
|
-
|
|
355
|
+
candidates.sort((a, b) => (order[a.priority] ?? 2) - (order[b.priority] ?? 2))
|
|
356
|
+
|
|
357
|
+
const budget = new ContextBudget(lim.max_total_bytes)
|
|
358
|
+
const entries = []
|
|
359
|
+
|
|
360
|
+
for (const c of candidates) {
|
|
361
|
+
const sectionBytes = Buffer.from(c.text, "utf8")
|
|
362
|
+
const fileCap = lim.max_file_bytes
|
|
363
|
+
const truncatedBytes = truncateUtf8(sectionBytes, fileCap)
|
|
364
|
+
let body = truncatedBytes.toString("utf8")
|
|
365
|
+
let mode = "inline"
|
|
366
|
+
if (truncatedBytes.length < sectionBytes.length) {
|
|
367
|
+
mode = "truncated"
|
|
368
|
+
body += truncateNotice(c.path, fileCap)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Budget the emitted block the same shape as buildContextFromEntries header+content
|
|
372
|
+
const header = `=== ${c.path} ===${c.reason ? ` (${c.reason})` : ""}`
|
|
373
|
+
const block = `${header}\n${body}`
|
|
374
|
+
const blockBytes = Buffer.byteLength(block, "utf8")
|
|
375
|
+
|
|
376
|
+
if (!budget.hasRoom(blockBytes)) {
|
|
377
|
+
const notice = indexNotice(c.path, c.originalByteSize, c.reason)
|
|
378
|
+
budget.add(Buffer.byteLength(notice, "utf8"))
|
|
379
|
+
entries.push({
|
|
380
|
+
path: c.path,
|
|
381
|
+
content: notice,
|
|
382
|
+
priority: c.priority,
|
|
383
|
+
kind: c.kind,
|
|
384
|
+
reason: c.reason,
|
|
385
|
+
mode: "index",
|
|
386
|
+
byteSize: Buffer.byteLength(notice, "utf8"),
|
|
387
|
+
originalByteSize: c.originalByteSize,
|
|
388
|
+
})
|
|
389
|
+
continue
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
budget.add(blockBytes)
|
|
393
|
+
entries.push({
|
|
394
|
+
path: c.path,
|
|
395
|
+
content: body,
|
|
396
|
+
priority: c.priority,
|
|
397
|
+
kind: c.kind,
|
|
398
|
+
reason: c.reason,
|
|
399
|
+
mode,
|
|
400
|
+
byteSize: Buffer.byteLength(body, "utf8"),
|
|
401
|
+
originalByteSize: c.originalByteSize,
|
|
402
|
+
})
|
|
403
|
+
}
|
|
195
404
|
|
|
196
405
|
return entries
|
|
197
406
|
}
|
|
@@ -206,7 +415,10 @@ export class AiwsContext {
|
|
|
206
415
|
if (!entries || entries.length === 0) return ""
|
|
207
416
|
return entries
|
|
208
417
|
.map(e => {
|
|
209
|
-
|
|
418
|
+
// index mode content is already a self-describing notice (no === header)
|
|
419
|
+
if (e.mode === "index") return e.content
|
|
420
|
+
const modeTag = e.mode && e.mode !== "inline" ? ` [mode=${e.mode}]` : ""
|
|
421
|
+
const header = `=== ${e.path} ===${e.reason ? ` (${e.reason})` : ""}${modeTag}`
|
|
210
422
|
return `${header}\n${e.content}`
|
|
211
423
|
})
|
|
212
424
|
.join("\n\n")
|
|
@@ -176,8 +176,20 @@ export default async ({ directory }) => {
|
|
|
176
176
|
return
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
const
|
|
180
|
-
|
|
179
|
+
const counts = { inline: 0, truncated: 0, index: 0 }
|
|
180
|
+
for (const e of entries) {
|
|
181
|
+
const m = e.mode || "inline"
|
|
182
|
+
if (m in counts) counts[m] += 1
|
|
183
|
+
else counts.inline += 1
|
|
184
|
+
}
|
|
185
|
+
const metaHeader =
|
|
186
|
+
`<aiws-context-meta>\n` +
|
|
187
|
+
`role: ${role}\n` +
|
|
188
|
+
`change: ${changeId}\n` +
|
|
189
|
+
`files: ${entries.length} (inline=${counts.inline}, truncated=${counts.truncated}, index=${counts.index})\n` +
|
|
190
|
+
`</aiws-context-meta>`
|
|
191
|
+
const contextBlock = `${metaHeader}\n\n${ctx.buildContextBlock(entries)}`
|
|
192
|
+
debugLog("inject", `Injected context for role "${role}", entries: ${entries.length}`, counts)
|
|
181
193
|
|
|
182
194
|
// Record delegation event to journal
|
|
183
195
|
const sessionKey = ctx.getSessionKey({ sessionID: input?.session_id || "" })
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* Reference: trellis/packages/cli/src/templates/opencode/plugins/inject-workflow-state.js
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { AiwsContext, AiwsSessionJournal, debugLog } from "../lib/aiws-context.js"
|
|
11
|
+
import { AiwsContext, AiwsSessionJournal, debugLog, promptHasSkipKeyword, resolveWorkflowSkipKeyword } from "../lib/aiws-context.js"
|
|
12
|
+
|
|
13
|
+
// Re-export for tests (TOOLING-003A / #427)
|
|
14
|
+
export { promptHasSkipKeyword, resolveWorkflowSkipKeyword }
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Build the workflow-state breadcrumb block.
|
|
@@ -79,6 +82,18 @@ export default async ({ directory }) => {
|
|
|
79
82
|
return
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
// Escape hatch (trellis #427): skip keyword in user prompt (default no-aiws)
|
|
86
|
+
const partsProbe = output?.parts || []
|
|
87
|
+
const originalText = partsProbe
|
|
88
|
+
.filter(p => p && p.type === "text" && typeof p.text === "string")
|
|
89
|
+
.map(p => p.text)
|
|
90
|
+
.join("\n")
|
|
91
|
+
const skipKeyword = resolveWorkflowSkipKeyword()
|
|
92
|
+
if (promptHasSkipKeyword(originalText, skipKeyword)) {
|
|
93
|
+
debugLog("workflow-state", "Skipping turn: skip keyword present in prompt", skipKeyword)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
if (!ctx.isAiwsProject()) {
|
|
83
98
|
return
|
|
84
99
|
}
|