@aipper/aiws-spec 0.0.29 → 0.0.30
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/docs/workflow-delegation-contracts.json +40 -1
- package/docs/workflow-delegation-contracts.md +10 -0
- package/docs/workflow-delegation-contracts.schema.json +57 -0
- package/docs/workflow-governance-rules.md +1 -1
- package/docs/workflow-review-gates.json +6 -0
- package/docs/workflow-review-gates.md +14 -5
- package/docs/workflow-review-gates.schema.json +4 -0
- package/docs/workflow-router-rules.json +46 -14
- package/docs/workflow-router-rules.md +19 -16
- package/docs/workflow-router-rules.schema.json +4 -0
- package/docs/workflow-stage-contracts.json +22 -2
- package/docs/workflow-stage-contracts.md +15 -13
- package/docs/workflow-stage-contracts.schema.json +12 -1
- package/package.json +1 -1
- package/templates/workspace/.agents/skills/using-aiws/SKILL.md +10 -6
- package/templates/workspace/.agents/skills/ws-commit/SKILL.md +5 -5
- package/templates/workspace/.agents/skills/ws-deliver/SKILL.md +5 -5
- package/templates/workspace/.agents/skills/ws-dev/SKILL.md +30 -0
- package/templates/workspace/.agents/skills/ws-finish/SKILL.md +9 -5
- package/templates/workspace/.agents/skills/ws-handoff/SKILL.md +5 -5
- package/templates/workspace/.agents/skills/ws-migrate/SKILL.md +18 -0
- package/templates/workspace/.agents/skills/ws-plan-verify/SKILL.md +5 -5
- package/templates/workspace/.agents/skills/ws-pull/SKILL.md +18 -0
- package/templates/workspace/.agents/skills/ws-push/SKILL.md +18 -0
- package/templates/workspace/.agents/skills/ws-submodule-setup/SKILL.md +18 -0
- package/templates/workspace/.agents/skills/ws-verify-before-complete/SKILL.md +5 -5
- package/templates/workspace/{changes → .aiws/changes}/README.md +13 -13
- package/templates/workspace/{changes → .aiws/changes}/templates/proposal.md +12 -12
- package/templates/workspace/{changes → .aiws/changes}/templates/tasks.md +8 -8
- package/templates/workspace/{issues → .aiws/issues}/problem-issues.csv +1 -1
- package/templates/workspace/.aiws/journal/.gitkeep +1 -0
- package/templates/workspace/{requirements → .aiws/requirements}/requirements-issues.csv +1 -1
- package/templates/workspace/.aiws/tmp/.gitkeep +0 -0
- package/templates/workspace/{tools → .aiws/tools}/requirements_contract.py +2 -2
- package/templates/workspace/{tools → .aiws/tools}/requirements_contract_sync.py +2 -2
- package/templates/workspace/{tools → .aiws/tools}/requirements_flow_gen.py +1 -1
- package/templates/workspace/{tools → .aiws/tools}/server_test_runner.py +10 -10
- package/templates/workspace/{tools → .aiws/tools}/ws_change_check.py +23 -23
- package/templates/workspace/{tools → .aiws/tools}/ws_resolve_sub_target.sh +2 -2
- package/templates/workspace/{tools → .aiws/tools}/ws_tasks_plan.py +3 -3
- package/templates/workspace/.claude/skills/ws-commit/SKILL.md +5 -5
- package/templates/workspace/.claude/skills/ws-deliver/SKILL.md +5 -5
- package/templates/workspace/.claude/skills/ws-finish/SKILL.md +9 -5
- package/templates/workspace/.claude/skills/ws-handoff/SKILL.md +5 -5
- package/templates/workspace/.claude/skills/ws-migrate/SKILL.md +18 -0
- package/templates/workspace/.claude/skills/ws-plan-verify/SKILL.md +5 -5
- package/templates/workspace/.claude/skills/ws-pull/SKILL.md +18 -0
- package/templates/workspace/.claude/skills/ws-push/SKILL.md +18 -0
- package/templates/workspace/.claude/skills/ws-submodule-setup/SKILL.md +18 -0
- package/templates/workspace/.claude/skills/ws-verify-before-complete/SKILL.md +5 -5
- package/templates/workspace/.githooks/pre-commit +13 -0
- package/templates/workspace/.githooks/pre-push +7 -0
- package/templates/workspace/.opencode/commands/ws-finish.md +66 -1
- package/templates/workspace/.opencode/helpers/approval-whitelist-check.sh +1 -1
- package/templates/workspace/.opencode/helpers/approval-whitelist-run.sh +1 -1
- package/templates/workspace/.opencode/helpers/approval-whitelist-watchdog.sh +1 -1
- package/templates/workspace/.opencode/helpers/tmux-swarm-rescue.sh +1 -1
- package/templates/workspace/.opencode/helpers/tmux-swarm-scan.sh +1 -1
- package/templates/workspace/.opencode/lib/aiws-context.js +953 -0
- package/templates/workspace/.opencode/plugins/aiws-inject-context.js +201 -0
- package/templates/workspace/.opencode/plugins/aiws-session-start.js +86 -0
- package/templates/workspace/.opencode/plugins/aiws-workflow-state.js +116 -0
- package/templates/workspace/.opencode/scripts/ws-plan-gen-submodule-targets.sh +27 -0
- package/templates/workspace/.opencode/scripts/ws-plan-setup-worktree.sh +31 -0
- package/templates/workspace/.opencode/skills/using-aiws/SKILL.md +39 -2
- package/templates/workspace/.opencode/skills/ws-commit/SKILL.md +5 -5
- package/templates/workspace/.opencode/skills/ws-delegate/SKILL.md +22 -0
- package/templates/workspace/.opencode/skills/ws-deliver/SKILL.md +5 -5
- package/templates/workspace/.opencode/skills/ws-dev/SKILL.md +52 -4
- package/templates/workspace/.opencode/skills/ws-dev-lite/SKILL.md +4 -1
- package/templates/workspace/.opencode/skills/ws-finish/SKILL.md +9 -5
- package/templates/workspace/.opencode/skills/ws-handoff/SKILL.md +5 -5
- package/templates/workspace/.opencode/skills/ws-intake/SKILL.md +32 -2
- package/templates/workspace/.opencode/skills/ws-migrate/SKILL.md +5 -5
- package/templates/workspace/.opencode/skills/ws-plan/SKILL.md +11 -53
- package/templates/workspace/.opencode/skills/ws-plan-verify/SKILL.md +5 -5
- package/templates/workspace/.opencode/skills/ws-pull/SKILL.md +5 -5
- package/templates/workspace/.opencode/skills/ws-push/SKILL.md +5 -5
- package/templates/workspace/.opencode/skills/ws-quality-review/SKILL.md +6 -1
- package/templates/workspace/.opencode/skills/ws-review/SKILL.md +25 -5
- package/templates/workspace/.opencode/skills/ws-submodule-setup/SKILL.md +5 -5
- package/templates/workspace/.opencode/skills/ws-verify-before-complete/SKILL.md +36 -5
- package/templates/workspace/AGENTS.md +32 -87
- package/templates/workspace/AI_PROJECT.md +4 -4
- package/templates/workspace/AI_WORKSPACE.md +1 -1
- package/templates/workspace/REQUIREMENTS.md +1 -1
- package/templates/workspace/gitignore +1 -1
- package/templates/workspace/manifest.json +96 -334
- /package/templates/workspace/{changes → .aiws/changes}/templates/design.md +0 -0
- /package/templates/workspace/{memory-bank → .aiws/memory-bank}/README.md +0 -0
- /package/templates/workspace/{memory-bank → .aiws/memory-bank}/architecture.md +0 -0
- /package/templates/workspace/{memory-bank → .aiws/memory-bank}/implementation-plan.md +0 -0
- /package/templates/workspace/{memory-bank → .aiws/memory-bank}/progress.md +0 -0
- /package/templates/workspace/{memory-bank → .aiws/memory-bank}/tech-stack.md +0 -0
- /package/templates/workspace/{requirements → .aiws/requirements}/CHANGELOG.md +0 -0
- /package/templates/workspace/{secrets → .aiws/secrets}/test-accounts.example.json +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aiws Context Injection Plugin
|
|
3
|
+
*
|
|
4
|
+
* Injects aiws context JSONL when task() is called with sub-agent types.
|
|
5
|
+
* Hook: tool.execute.before — intercepts task() calls, reads <role>-context.jsonl
|
|
6
|
+
* from changes/<id>/analysis/, and injects into the agent prompt.
|
|
7
|
+
*
|
|
8
|
+
* Reference: trellis/packages/cli/src/templates/opencode/plugins/inject-subagent-context.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync } from "fs"
|
|
12
|
+
import { join } from "path"
|
|
13
|
+
import { AiwsContext, AiwsSessionJournal, debugLog } from "../lib/aiws-context.js"
|
|
14
|
+
|
|
15
|
+
// Role detection — check args.subagent_type first, then fall back to prompt marker
|
|
16
|
+
// Prompt should include: "role: <role>" as a line in the prompt
|
|
17
|
+
const ROLE_MARKER_RE = /^\s*role\s*[:=]\s*(\w+)\s*$/im
|
|
18
|
+
|
|
19
|
+
function detectRole(args, originalPrompt) {
|
|
20
|
+
// Priority 1: args.subagent_type (set by task() caller)
|
|
21
|
+
if (args?.subagent_type) {
|
|
22
|
+
const t = String(args.subagent_type).toLowerCase().replace(/^(?:trellis|aiws)-/, "")
|
|
23
|
+
if (["worker", "explorer", "reviewer", "planner"].includes(t)) return t
|
|
24
|
+
}
|
|
25
|
+
// Priority 2: "role:" marker in prompt body
|
|
26
|
+
const roleMatch = originalPrompt?.match(ROLE_MARKER_RE)
|
|
27
|
+
if (roleMatch) return roleMatch[1].toLowerCase()
|
|
28
|
+
// Priority 3: "subagent_type" or "agent_type" in prompt body
|
|
29
|
+
const typeMatch = originalPrompt?.match(/\b(?:subagent_type|agent_type)\s*[:=]\s*(\w+)\s*/im)
|
|
30
|
+
if (typeMatch) return typeMatch[1].toLowerCase().replace(/^trellis-/, "")
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const FIRST_REPLY_INSTRUCTION = `<aiws-context-instruction>
|
|
35
|
+
After reading the JSONL context files, confirm by listing the files you read and their key points (≤3 per file).
|
|
36
|
+
|
|
37
|
+
**Context file path:** changes/<change-id>/analysis/<role>-context.jsonl
|
|
38
|
+
|
|
39
|
+
**Execution order:**
|
|
40
|
+
1. Read the JSONL file with the Read tool
|
|
41
|
+
2. Follow priority order: high → medium → low
|
|
42
|
+
3. If sections defined, only read those line ranges for files > 200 lines
|
|
43
|
+
4. If any file is inaccessible, flag it but continue with what's available
|
|
44
|
+
|
|
45
|
+
**Your first response** must begin with:
|
|
46
|
+
\`\`\`
|
|
47
|
+
Read context files: [list of files read]
|
|
48
|
+
Key points: [brief summary per file]
|
|
49
|
+
Status: READY | NEEDS_CONTEXT
|
|
50
|
+
\`\`\`
|
|
51
|
+
</aiws-context-instruction>`
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the active change ID by scanning changes/ directory.
|
|
55
|
+
*/
|
|
56
|
+
function resolveActiveChangeId(ctx) {
|
|
57
|
+
const active = ctx.getActiveChange()
|
|
58
|
+
return active ? active.id : null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Find JSONL context file for a given role in the active change.
|
|
63
|
+
*/
|
|
64
|
+
function resolveRoleJsonl(ctx, changeId, role) {
|
|
65
|
+
if (!changeId || !role) return null
|
|
66
|
+
const jsonlPath = join(ctx.directory, "changes", changeId, "analysis", `${role}-context.jsonl`)
|
|
67
|
+
return existsSync(jsonlPath) ? jsonlPath : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build fallback context when no curated JSONL exists (G7).
|
|
72
|
+
* Injects at minimum: change ID, phase, spec index.
|
|
73
|
+
*/
|
|
74
|
+
function buildFallbackContext(ctx, changeId, role) {
|
|
75
|
+
const parts = []
|
|
76
|
+
parts.push(`<aiws-context-fallback>
|
|
77
|
+
No curated context JSONL for role "${role}" in change "${changeId}".
|
|
78
|
+
Using project context as minimal scaffold.
|
|
79
|
+
</aiws-context-fallback>`)
|
|
80
|
+
|
|
81
|
+
const changeDetail = ctx.getChangeDetail(changeId)
|
|
82
|
+
if (changeDetail) {
|
|
83
|
+
const lines = [
|
|
84
|
+
`Change: ${changeDetail.id}`,
|
|
85
|
+
`Phase: ${changeDetail.phase}`,
|
|
86
|
+
]
|
|
87
|
+
if (changeDetail.planFiles.length > 0) {
|
|
88
|
+
lines.push(`Plans: ${changeDetail.planFiles.join(", ")}`)
|
|
89
|
+
}
|
|
90
|
+
parts.push(`<change-context>\n${lines.join("\n")}\n</change-context>`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const specIndex = ctx.getSpecIndex()
|
|
94
|
+
if (specIndex) {
|
|
95
|
+
parts.push(`<spec-index>\n${specIndex.trimEnd()}\n</spec-index>`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
parts.push(`<aiws-context-instruction>
|
|
99
|
+
No JSONL context file was found for your role.
|
|
100
|
+
Review the change context and spec index above for relevant constraints.
|
|
101
|
+
Focus on the spec documents that apply to your task.
|
|
102
|
+
If you need more context, use the Glob and Grep tools to discover the codebase.
|
|
103
|
+
</aiws-context-instruction>`)
|
|
104
|
+
|
|
105
|
+
return parts.join("\n\n")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Write journal entry for a fallback context injection.
|
|
110
|
+
* Extracted to avoid repeating the sessionKey/journal creation in both fallback paths.
|
|
111
|
+
*/
|
|
112
|
+
function _writeFallbackJournal(ctx, input, role, changeId, reason) {
|
|
113
|
+
const sessionKey = ctx.getSessionKey({ sessionID: input?.session_id || "" })
|
|
114
|
+
if (sessionKey) {
|
|
115
|
+
try {
|
|
116
|
+
const journal = new AiwsSessionJournal(ctx.directory)
|
|
117
|
+
journal.writeDelegation(sessionKey, role, changeId, {
|
|
118
|
+
contextFiles: 0,
|
|
119
|
+
hasFallback: true,
|
|
120
|
+
reason,
|
|
121
|
+
})
|
|
122
|
+
} catch {
|
|
123
|
+
// journal write failure should never break plugin flow
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default async ({ directory }) => {
|
|
129
|
+
const ctx = new AiwsContext(directory)
|
|
130
|
+
debugLog("inject", "Plugin loaded, directory:", directory)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"tool.execute.before": async (input, output) => {
|
|
134
|
+
try {
|
|
135
|
+
if (process.env.AIWS_HOOKS === "0" || process.env.AIWS_DISABLE_HOOKS === "1") {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const toolName = input?.tool?.toLowerCase()
|
|
140
|
+
if (toolName !== "task") return
|
|
141
|
+
|
|
142
|
+
const args = output?.args
|
|
143
|
+
if (!args) return
|
|
144
|
+
|
|
145
|
+
const originalPrompt = args.prompt || ""
|
|
146
|
+
if (!originalPrompt) return
|
|
147
|
+
|
|
148
|
+
// Extract role using detectRole (checks subagent_type first, then prompt markers)
|
|
149
|
+
const role = detectRole(args, originalPrompt) || "worker"
|
|
150
|
+
debugLog("inject", "Task tool called, role:", role)
|
|
151
|
+
|
|
152
|
+
// Resolve active change
|
|
153
|
+
const changeId = resolveActiveChangeId(ctx)
|
|
154
|
+
if (!changeId) {
|
|
155
|
+
debugLog("inject", "No active change found, skipping")
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Find context JSONL for this role
|
|
160
|
+
const jsonlPath = resolveRoleJsonl(ctx, changeId, role)
|
|
161
|
+
if (!jsonlPath) {
|
|
162
|
+
debugLog("inject", `No context JSONL for role "${role}" in change "${changeId}", injecting fallback`)
|
|
163
|
+
_writeFallbackJournal(ctx, input, role, changeId, "no_jsonl_file")
|
|
164
|
+
const fallbackCtx = buildFallbackContext(ctx, changeId, role)
|
|
165
|
+
args.prompt = `${fallbackCtx}\n\n---\n\n${originalPrompt}`
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Read JSONL and build context
|
|
170
|
+
const entries = ctx.readJsonlWithFiles(jsonlPath)
|
|
171
|
+
if (entries.length === 0) {
|
|
172
|
+
debugLog("inject", `Empty context JSONL for role "${role}" in change "${changeId}", injecting fallback`)
|
|
173
|
+
_writeFallbackJournal(ctx, input, role, changeId, "empty_jsonl")
|
|
174
|
+
const fallbackCtx = buildFallbackContext(ctx, changeId, role)
|
|
175
|
+
args.prompt = `${fallbackCtx}\n\n---\n\n${originalPrompt}`
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const contextBlock = ctx.buildContextBlock(entries)
|
|
180
|
+
debugLog("inject", `Injected context for role "${role}", entries: ${entries.length}`)
|
|
181
|
+
|
|
182
|
+
// Record delegation event to journal
|
|
183
|
+
const sessionKey = ctx.getSessionKey({ sessionID: input?.session_id || "" })
|
|
184
|
+
if (sessionKey) {
|
|
185
|
+
const journal = new AiwsSessionJournal(ctx.directory)
|
|
186
|
+
journal.writeDelegation(sessionKey, role, changeId, {
|
|
187
|
+
contextFiles: entries.length,
|
|
188
|
+
originalPromptSnippet: originalPrompt.slice(0, 120),
|
|
189
|
+
hasFallback: false,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Prepend context + instruction to the prompt
|
|
194
|
+
args.prompt = `${contextBlock}\n\n${FIRST_REPLY_INSTRUCTION}\n\n---\n\n${originalPrompt}`
|
|
195
|
+
|
|
196
|
+
} catch (error) {
|
|
197
|
+
debugLog("inject", "Error in tool.execute.before:", error.message, error.stack)
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aiws Session Start Plugin
|
|
3
|
+
*
|
|
4
|
+
* Injects aiws context (spec index, change status, handoff) when user
|
|
5
|
+
* sends the first message in a new session.
|
|
6
|
+
*
|
|
7
|
+
* Hook: chat.message — modifies the output to prepend context.
|
|
8
|
+
*
|
|
9
|
+
* Reference: trellis/packages/cli/src/templates/opencode/plugins/session-start.js
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { AiwsContext, buildSessionContext, contextCollector, debugLog } from "../lib/aiws-context.js"
|
|
13
|
+
|
|
14
|
+
export default async ({ directory, client }) => {
|
|
15
|
+
const ctx = new AiwsContext(directory)
|
|
16
|
+
debugLog("session", "Plugin loaded, directory:", directory)
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
event: ({ event }) => {
|
|
20
|
+
try {
|
|
21
|
+
if (event?.type === "session.compacted" && event?.properties?.sessionID) {
|
|
22
|
+
contextCollector.clear(event.properties.sessionID)
|
|
23
|
+
debugLog("session", "Cleared processed flag after compaction for session:", event.properties.sessionID)
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
debugLog("session", "Error in event hook:", error instanceof Error ? error.message : String(error))
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
"chat.message": async (input, output) => {
|
|
31
|
+
try {
|
|
32
|
+
const sessionKey = ctx.getSessionKey(input)
|
|
33
|
+
if (!sessionKey) {
|
|
34
|
+
debugLog("session", "No session key available, skipping")
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const agent = input.agent || ""
|
|
39
|
+
if (agent && (agent.startsWith("trellis-") || agent.startsWith("aiws-"))) {
|
|
40
|
+
debugLog("session", "Skipping sub-agent turn:", agent)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (process.env.AIWS_HOOKS === "0" || process.env.AIWS_DISABLE_HOOKS === "1") {
|
|
45
|
+
debugLog("session", "Skipping — AIWS_HOOKS disabled")
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (process.env.OPENCODE_NON_INTERACTIVE === "1") {
|
|
50
|
+
debugLog("session", "Skipping — non-interactive mode")
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (contextCollector.isProcessed(sessionKey)) {
|
|
55
|
+
debugLog("session", "Skipping — session already processed:", sessionKey)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const context = buildSessionContext(ctx, input)
|
|
60
|
+
debugLog("session", "Built context, length:", context.length)
|
|
61
|
+
|
|
62
|
+
if (!context) {
|
|
63
|
+
debugLog("session", "Empty context, nothing to inject")
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const parts = output?.parts || []
|
|
68
|
+
const textPartIndex = parts.findIndex(p => p.type === "text" && p.text !== undefined)
|
|
69
|
+
|
|
70
|
+
if (textPartIndex !== -1) {
|
|
71
|
+
const originalText = parts[textPartIndex].text || ""
|
|
72
|
+
parts[textPartIndex].text = `${context}\n\n---\n\n${originalText}`
|
|
73
|
+
debugLog("session", "Injected context into chat.message text part, length:", context.length)
|
|
74
|
+
} else {
|
|
75
|
+
const injectedPart = { type: "text", text: context }
|
|
76
|
+
parts.unshift(injectedPart)
|
|
77
|
+
debugLog("session", "Prepended new text part with context, length:", context.length)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
contextCollector.markProcessed(sessionKey)
|
|
81
|
+
} catch (error) {
|
|
82
|
+
debugLog("session", "Error in chat.message:", error.message, error.stack)
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aiws Workflow State Injection Plugin
|
|
3
|
+
*
|
|
4
|
+
* Per-turn chat.message breadcrumb showing active change + current phase.
|
|
5
|
+
* Unlike session-start, this fires on EVERY user message so long
|
|
6
|
+
* conversations don't lose context of what's being worked on.
|
|
7
|
+
*
|
|
8
|
+
* Reference: trellis/packages/cli/src/templates/opencode/plugins/inject-workflow-state.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { AiwsContext, AiwsSessionJournal, debugLog } from "../lib/aiws-context.js"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build the workflow-state breadcrumb block.
|
|
15
|
+
*/
|
|
16
|
+
function buildBreadcrumb(change, phase) {
|
|
17
|
+
const parts = []
|
|
18
|
+
parts.push("<workflow-state>")
|
|
19
|
+
|
|
20
|
+
if (change) {
|
|
21
|
+
parts.push(`Active Change: ${change.id}`)
|
|
22
|
+
parts.push(`Change Dir: changes/${change.id}/`)
|
|
23
|
+
|
|
24
|
+
if (change.planFiles.length > 0) {
|
|
25
|
+
parts.push(`Plans: ${change.planFiles.slice(0, 3).join(", ")}`)
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
parts.push("Active Change: none")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (phase) {
|
|
32
|
+
parts.push(`Phase: ${phase} (${AiwsContext.PHASES.indexOf(phase) + 1}/${AiwsContext.PHASES.length})`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Phase-specific next actions — aligned with AiwsContext.PHASES state machine
|
|
36
|
+
// Subagent-first policy: ready-for-dev defaults to aiws-worker; in-progress defaults to aiws-reviewer
|
|
37
|
+
const nextActions = {
|
|
38
|
+
intake: "Next: ws-plan to create a change and execution plan",
|
|
39
|
+
planning: "Next: ws-plan-verify to review the plan, or ws-dev to begin implementation",
|
|
40
|
+
"ready-for-dev": "Next: dispatch aiws-worker (subagent-first); main session coordinates, does not write code directly",
|
|
41
|
+
"in-progress": "Next: dispatch aiws-reviewer + ws-review to audit changes",
|
|
42
|
+
review: "Next: ws-commit to validate and commit",
|
|
43
|
+
finished: "Next: ws-finish to finalize and archive the change",
|
|
44
|
+
}
|
|
45
|
+
if (phase && nextActions[phase]) {
|
|
46
|
+
parts.push(nextActions[phase])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!change && !phase) {
|
|
50
|
+
parts.push("No active work detected. Start with ws-preflight or ws-plan.")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Available phases reference — aligned with AiwsContext.PHASES
|
|
54
|
+
parts.push(`Phases: ${AiwsContext.PHASES.join(" → ")}`)
|
|
55
|
+
|
|
56
|
+
parts.push("</workflow-state>")
|
|
57
|
+
return parts.join("\n")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default async ({ directory }) => {
|
|
61
|
+
const ctx = new AiwsContext(directory)
|
|
62
|
+
debugLog("workflow-state", "Plugin loaded, directory:", directory)
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"chat.message": async (input, output) => {
|
|
66
|
+
try {
|
|
67
|
+
// Skip sub-agent turns
|
|
68
|
+
const agent = input?.agent || ""
|
|
69
|
+
if (agent.startsWith("trellis-") || agent.startsWith("aiws-")) {
|
|
70
|
+
debugLog("workflow-state", "Skipping subagent turn:", agent)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (process.env.AIWS_HOOKS === "0" || process.env.AIWS_DISABLE_HOOKS === "1") {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (process.env.OPENCODE_NON_INTERACTIVE === "1") {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!ctx.isAiwsProject()) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const change = ctx.getActiveChange()
|
|
87
|
+
const phase = change ? change.phase : null
|
|
88
|
+
const breadcrumb = buildBreadcrumb(change, phase)
|
|
89
|
+
|
|
90
|
+
// Inject into the output text part (no dedup — show every turn)
|
|
91
|
+
const parts = output?.parts || []
|
|
92
|
+
const textIdx = parts.findIndex(p => p.type === "text" && p.text !== undefined)
|
|
93
|
+
if (textIdx !== -1) {
|
|
94
|
+
parts[textIdx].text = `${breadcrumb}\n\n${parts[textIdx].text}`
|
|
95
|
+
} else {
|
|
96
|
+
parts.unshift({ type: "text", text: breadcrumb })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
debugLog("workflow-state", "Injected breadcrumb:", change?.id || "none", "phase:", phase || "none")
|
|
100
|
+
|
|
101
|
+
// Record breadcrumb event to journal (non-blocking)
|
|
102
|
+
try {
|
|
103
|
+
const sessionKey = ctx.getSessionKey({ sessionID: input?.session_id || "" })
|
|
104
|
+
if (sessionKey) {
|
|
105
|
+
const journal = new AiwsSessionJournal(ctx.directory)
|
|
106
|
+
journal.writeBreadcrumb(sessionKey, phase || "unknown", change?.id || null)
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Journal write failure should never break the plugin
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
debugLog("workflow-state", "Error in chat.message:", error instanceof Error ? error.message : String(error))
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ws-plan: generate submodules.targets for the current change
|
|
3
|
+
# Usage: bash ws-plan-gen-submodule-targets.sh <change-id>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
change_id="${1:?missing change_id}"
|
|
7
|
+
targets=".aiws/changes/${change_id}/submodules.targets"
|
|
8
|
+
|
|
9
|
+
mkdir -p ".aiws/changes/${change_id}"
|
|
10
|
+
|
|
11
|
+
if [[ -f "${targets}" ]]; then
|
|
12
|
+
bak="${targets}.bak.$(date -u +%Y%m%d-%H%M%SZ)"
|
|
13
|
+
cp "${targets}" "${bak}"
|
|
14
|
+
echo "info: backup: ${bak}"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
: > "${targets}"
|
|
18
|
+
echo "# path<TAB>target_branch<TAB>remote(optional, default=origin)" >> "${targets}"
|
|
19
|
+
|
|
20
|
+
while read -r key sub_path; do
|
|
21
|
+
name="${key#submodule.}"; name="${name%.path}"
|
|
22
|
+
b="$(git config --file .gitmodules --get "submodule.${name}.branch" 2>/dev/null || true)"
|
|
23
|
+
[[ "${b:-}" == "." ]] && b="$(git branch --show-current)" # '.' means "follow superproject branch"
|
|
24
|
+
printf "%s\t%s\t%s\n" "${sub_path}" "${b:-<fill-me>}" "origin" >> "${targets}"
|
|
25
|
+
done < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null || true)
|
|
26
|
+
|
|
27
|
+
echo "ok: wrote ${targets}"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ws-plan: create change context with worktree/submodule support
|
|
3
|
+
# Usage: bash ws-plan-setup-worktree.sh <change-id>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
change_id="${1:?missing change_id}"
|
|
7
|
+
|
|
8
|
+
# Guard: refuse to create context on dirty working tree
|
|
9
|
+
if [[ -n "$(git status --porcelain)" ]]; then
|
|
10
|
+
echo "error: working tree dirty before ws-plan creates change context"
|
|
11
|
+
echo "hint: commit/stash first, or continue inside an existing change/<change-id> context"
|
|
12
|
+
exit 2
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
has_commits=0
|
|
16
|
+
git rev-parse --verify HEAD >/dev/null 2>&1 && has_commits=1
|
|
17
|
+
|
|
18
|
+
has_submodules=0
|
|
19
|
+
if [[ -f .gitmodules ]] && git config --file .gitmodules --get-regexp '^submodule\..*\.path$' >/dev/null 2>&1; then
|
|
20
|
+
has_submodules=1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if [[ "${has_commits}" -eq 1 ]]; then
|
|
24
|
+
if [[ "${has_submodules}" -eq 1 ]]; then
|
|
25
|
+
aiws change start "${change_id}" --hooks --worktree --submodules
|
|
26
|
+
else
|
|
27
|
+
aiws change start "${change_id}" --hooks --worktree
|
|
28
|
+
fi
|
|
29
|
+
else
|
|
30
|
+
aiws change start "${change_id}" --hooks --no-switch
|
|
31
|
+
fi
|
|
@@ -7,6 +7,12 @@ description: 使用时机:新会话开始、不确定下一步做什么时。
|
|
|
7
7
|
|
|
8
8
|
目标:默认入口,先读真值文件,判定当前任务 workflow,再进入具体 `ws-*` skill。若意图不明确:先澄清,不直接进入实现。
|
|
9
9
|
|
|
10
|
+
## 编排约束
|
|
11
|
+
|
|
12
|
+
- **不直接实现**:`using-aiws` 自身不是实现阶段;只判 workflow、读真值、路由到对应 `ws-*`。主 session 不直接写代码。
|
|
13
|
+
- **上下文先于判决**:路由 `direct_implementation` 前必须先收集项目上下文——具体检查:`git status --porcelain`(已改动文件数)、`git diff --stat`(改动行范围)、`AI_WORKSPACE.md` 中声明的验证命令可行性。仅当改动文件数 ≤ 3 且改动总行数 ≤ 100 且验证入口明确时,才可判 direct。
|
|
14
|
+
- **意图不明先澄清**:`routeTo=clarify` 时必须停止并问 1-3 个关键问题,不猜测。
|
|
15
|
+
|
|
10
16
|
阶段定位:bootstrap/router 阶段;负责分流,不直接完成实现。
|
|
11
17
|
|
|
12
18
|
## 必需输入
|
|
@@ -36,24 +42,47 @@ description: 使用时机:新会话开始、不确定下一步做什么时。
|
|
|
36
42
|
- 若未检测到:输出 `standard-opencode`
|
|
37
43
|
- 若缺失任一真值文件 → route = `$ws-preflight`,建议先 `aiws init .`
|
|
38
44
|
|
|
45
|
+
### 1.5 Per-turn Breadcrumb(必做)
|
|
46
|
+
|
|
47
|
+
每轮对话开始时强制读取当前 change 状态(`changes/<change-id>/.ws-change.json` 或 phase 状态文件),输出 `[workflow-state:PHASE/N]` breadcrumb 标记。
|
|
48
|
+
|
|
49
|
+
目的:即使 context 被压缩,breadcrumb 也能让 AI 知道当前处于哪个阶段、上次做到哪一步。
|
|
50
|
+
|
|
51
|
+
格式:`[workflow-state:PHASE_NAME/N]`,其中 PHASE_NAME 取自 standardChain,N 为步骤序号。
|
|
52
|
+
|
|
39
53
|
### 2. 路由判定
|
|
40
54
|
|
|
41
55
|
根据 `packages/spec/docs/workflow-router-rules.json` 判定:
|
|
42
56
|
|
|
57
|
+
- 路由前先读上下文:判 direct_implementation 前,router 必须先收集项目上下文(git status、涉及文件范围),确认是真正的单文件修复,再决定 direct vs plan。
|
|
58
|
+
|
|
59
|
+
上下文收集清单(判 direct_implementation 前必须完成):
|
|
60
|
+
- `git status --porcelain` — 确认未提交改动范围
|
|
61
|
+
- `git diff --stat` — 确认改动文件数量
|
|
62
|
+
- 改动涉及 ≤2 文件且均在已知路径 → 可判 direct
|
|
63
|
+
- 改动涉及 ≥3 文件或含未知路径 → 必须走 ws-plan
|
|
64
|
+
|
|
43
65
|
| 意图 | Route |
|
|
44
66
|
|------|-------|
|
|
45
67
|
| 需求/验收/合同变更 | `$ws-req-review` |
|
|
46
68
|
| 评审/审计/找风险 | `$ws-review` |
|
|
47
69
|
| finish/merge/push/cleanup | `$ws-finish` |
|
|
48
70
|
| handoff/archive | `$ws-handoff` |
|
|
49
|
-
| 先出设计方案 | `$ws-
|
|
50
|
-
| 更新规范/验收标准 | `$ws-
|
|
71
|
+
| 先出设计方案 | `$ws-plan`(设计作为 plan 阶段一部分) |
|
|
72
|
+
| 更新规范/验收标准 | `$ws-req-change`(需求变更需先 review) |
|
|
51
73
|
| 中大型实现 | `$ws-plan` |
|
|
52
74
|
| 小步明确实现/修复 | `$ws-dev` |
|
|
53
75
|
| 极简修复 | `$ws-dev-lite` |
|
|
76
|
+
| Subagent 不可用 | 回退单 agent + 工件模式 |
|
|
54
77
|
|
|
55
78
|
注:`$ws-dev` 默认走 subagent-first 策略(详见 `packages/spec/docs/opencode-subagent-first.md`)。主 session 应优先通过 `$ws-delegate` 派发 `aiws-worker`,除非用户明确说"直接改"或"do it inline"。
|
|
56
79
|
|
|
80
|
+
**Escape Hatch**:若用户明确说"跳过流程"/"直接改"/"do it inline",允许走 `direct_implementation` 路由,但必须:
|
|
81
|
+
1. 输出 `[escape-hatch: direct-implementation]` 标记
|
|
82
|
+
2. 仍须归因到 Req_ID / Problem_ID
|
|
83
|
+
3. 仍须有可复现验证入口
|
|
84
|
+
4. 仍须落盘 evidence 标记 escape-hatch 使用原因
|
|
85
|
+
|
|
57
86
|
### 3. 意图不明确
|
|
58
87
|
|
|
59
88
|
只问 1-3 个关键问题,明确缺什么(意图、Req_ID/Problem_ID、verify、change 上下文),然后停止。
|
|
@@ -82,10 +111,17 @@ description: 使用时机:新会话开始、不确定下一步做什么时。
|
|
|
82
111
|
└──────────────────┴──────────────────────────────────────────────┘
|
|
83
112
|
```
|
|
84
113
|
|
|
114
|
+
Subagent 不可用时的降级路径:
|
|
115
|
+
- ready-for-dev 无 subagent → 当前 agent 直接执行 ws-dev(走 inline escape hatch)
|
|
116
|
+
- in-progress 无 reviewer → 当前 agent 自审(走 evaluate-optimize 1 轮)
|
|
117
|
+
- review 无 oracle → 当前 agent 本地 review(不阻断流程)
|
|
118
|
+
|
|
85
119
|
特殊状态:
|
|
86
120
|
- 上次 delegation 返回 `BLOCKED` → 先解决 blocker 再继续
|
|
87
121
|
- 上次 delegation 返回 `NEEDS_CONTEXT` → 补 context JSONL 后重新派发
|
|
88
122
|
- Subagent-first 默认:`ready-for-dev` / `in-progress` 默认派发 worker/reviewer
|
|
123
|
+
- Subagent 不可用时:降级为当前 agent 直接执行,但必须在 evidence 中记录降级原因
|
|
124
|
+
- Subagent 不可用时回退:单 agent 模式 + 显式维护 `changes/<id>/` 工件(handoff-evidence.md, analysis/, patches/);不阻断流程,但须标注 `mode: single-agent`
|
|
89
125
|
|
|
90
126
|
### 5. 输出路由
|
|
91
127
|
|
|
@@ -105,3 +141,4 @@ Next: <继续执行对应 skill,或提出澄清问题>
|
|
|
105
141
|
- Router 自己不实现代码;不给 route 前不直接改代码
|
|
106
142
|
- 一次只选一个主 route
|
|
107
143
|
- 若复杂度升高:`$ws-dev` 回退到 `$ws-plan`;`$ws-finish` 回退到前置门禁
|
|
144
|
+
- 若 subagent 不可用:回退为单 agent 执行,但须维护与 subagent 模式等价的工件结构(handoff、evidence、review 文件),确保后续 session 可接力
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ws-commit
|
|
3
|
-
description: Thin wrapper for `aiws
|
|
3
|
+
description: Thin wrapper for `aiws commit`
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# ws-commit
|
|
7
7
|
|
|
8
|
-
Thin skill wrapper. Delegates to `aiws
|
|
8
|
+
Thin skill wrapper. Delegates to `aiws commit`. See `aiws commit --help` for details.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
if [[ -x "./node_modules/.bin/aiws" ]]; then
|
|
12
|
-
./node_modules/.bin/aiws
|
|
12
|
+
./node_modules/.bin/aiws commit
|
|
13
13
|
elif command -v aiws >/dev/null 2>&1; then
|
|
14
|
-
aiws
|
|
14
|
+
aiws commit
|
|
15
15
|
else
|
|
16
|
-
npx @aipper/aiws
|
|
16
|
+
npx @aipper/aiws commit
|
|
17
17
|
fi
|
|
18
18
|
```
|
|
@@ -7,6 +7,11 @@ description: 使用时机:需要拆分子任务、委托给子 agent 时。触
|
|
|
7
7
|
|
|
8
8
|
目标:在 OpenCode 中,优先借用 `oh-my-opencode` 的现有 agent 做任务拆分;若 oMo 不可用,再回退为普通 OpenCode delegation / 单 agent 执行。
|
|
9
9
|
|
|
10
|
+
## 核心约束
|
|
11
|
+
|
|
12
|
+
- **Subagent-First**:主 session 只做编排与收敛,不直接写实现代码。所有产出必须由 subagent 完成并可追溯到具体 worker。
|
|
13
|
+
- **Handoff 证据**:每轮委托完成后,worker 必须产出结构化 handoff 到 `changes/<id>/handoff-evidence.md`(完成项、未完成项、残余风险),供主 session 收敛判断。handoff 文件缺失等同于委托未完成。
|
|
14
|
+
|
|
10
15
|
## 必需输入
|
|
11
16
|
|
|
12
17
|
- 真值文件:`AI_PROJECT.md` / `REQUIREMENTS.md` / `AI_WORKSPACE.md`
|
|
@@ -24,12 +29,18 @@ description: 使用时机:需要拆分子任务、委托给子 agent 时。触
|
|
|
24
29
|
- `Evidence:` 产物路径
|
|
25
30
|
- `Next:` 回到 `ws-dev` / `ws-review` / `ws-commit` / `ws-finish`
|
|
26
31
|
|
|
32
|
+
## 执行要求
|
|
33
|
+
|
|
34
|
+
- 主 session 不直接改代码:所有实现与验证产物必须由 subagent 产出并向 handoff 记录可追溯
|
|
35
|
+
- handoff 证据:手交材料中须含 delegate round number、产出文件路径、已知未关闭项
|
|
36
|
+
|
|
27
37
|
## 阻断条件
|
|
28
38
|
|
|
29
39
|
- 任务未绑定
|
|
30
40
|
- 没有写清委托边界
|
|
31
41
|
- 上下文策展未执行(未生成 JSONL 或未在 prompt 中引用)
|
|
32
42
|
- 无法判断当前是否可用 oMo,又不能接受回退
|
|
43
|
+
- handoff 文件 `changes/<id>/handoff-evidence.md` 缺失或为空(委托返回后必须检查)
|
|
33
44
|
|
|
34
45
|
## 角色映射
|
|
35
46
|
|
|
@@ -40,6 +51,16 @@ description: 使用时机:需要拆分子任务、委托给子 agent 时。触
|
|
|
40
51
|
| `reviewer` | `@oracle` |
|
|
41
52
|
| `integrator` | 当前主 agent |
|
|
42
53
|
|
|
54
|
+
**推荐标准角色**(source: `workflow-delegation-contracts.json` standardRoles):
|
|
55
|
+
|
|
56
|
+
| 角色 | 职责 | 读取范围 | 写入范围 |
|
|
57
|
+
|------|------|----------|----------|
|
|
58
|
+
| `implementer` | 代码+测试实现 | 真值文件+change上下文 | 代码文件+测试文件+evidence/ |
|
|
59
|
+
| `reviewer` | 独立审查 | 真值文件+diff+evidence | review/*.md |
|
|
60
|
+
| `researcher` | 分析探索 | 真值文件+外部文档 | analysis/*.md |
|
|
61
|
+
|
|
62
|
+
这些角色为建议非强制;委托时优先参考但允许根据任务需要调整。
|
|
63
|
+
|
|
43
64
|
## 连续执行循环(Worker → Reviewer → Fix)
|
|
44
65
|
|
|
45
66
|
默认闭环(详见 `packages/spec/docs/opencode-subagent-first.md`):
|
|
@@ -109,6 +130,7 @@ Context Curation: changes/<id>/analysis/worker-context.jsonl
|
|
|
109
130
|
- [ ] 解析 Status 行
|
|
110
131
|
- [ ] 非 DONE → 按状态处理规则行动
|
|
111
132
|
- [ ] 非 DONE → 记录决策到 `delegation-decisions.md`
|
|
133
|
+
- [ ] handoff 文件 `changes/<id>/handoff-evidence.md` 已存在且非空
|
|
112
134
|
|
|
113
135
|
安全:
|
|
114
136
|
- 不让 `ws-delegate` 变成第二套 orchestrator
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ws-deliver
|
|
3
|
-
description: Thin wrapper for `aiws
|
|
3
|
+
description: Thin wrapper for `aiws deliver`
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# ws-deliver
|
|
7
7
|
|
|
8
|
-
Thin skill wrapper. Delegates to `aiws
|
|
8
|
+
Thin skill wrapper. Delegates to `aiws deliver`. See `aiws deliver --help` for details.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
if [[ -x "./node_modules/.bin/aiws" ]]; then
|
|
12
|
-
./node_modules/.bin/aiws
|
|
12
|
+
./node_modules/.bin/aiws deliver
|
|
13
13
|
elif command -v aiws >/dev/null 2>&1; then
|
|
14
|
-
aiws
|
|
14
|
+
aiws deliver
|
|
15
15
|
else
|
|
16
|
-
npx @aipper/aiws
|
|
16
|
+
npx @aipper/aiws deliver
|
|
17
17
|
fi
|
|
18
18
|
```
|