pi-crew 0.5.24 → 0.6.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/CHANGELOG.md +34 -0
- package/docs/patterns/command-agent-skill.md +71 -0
- package/package.json +1 -1
- package/skills/council/SKILL.md +163 -0
- package/src/agents/agent-config.ts +2 -0
- package/src/agents/discover-agents.ts +1 -0
- package/src/hooks/types.ts +14 -0
- package/src/runtime/chain-parser.ts +192 -0
- package/src/runtime/drift-detectors.ts +220 -0
- package/src/runtime/intercom-bridge.ts +178 -0
- package/src/runtime/plan-templates.ts +200 -0
- package/src/runtime/task-graph.ts +79 -0
- package/src/runtime/task-id.ts +148 -0
- package/src/runtime/task-runner/context-retrieval.ts +172 -0
- package/src/runtime/task-runner.ts +30 -1
- package/src/runtime/team-runner.ts +7 -0
- package/src/state/memory-store.ts +244 -0
- package/src/state/observation-store.ts +177 -0
- package/src/utils/fingerprint.ts +183 -0
- package/src/workflows/discover-workflows.ts +5 -1
- package/src/workflows/intermediate-store.ts +173 -0
- package/src/workflows/workflow-config.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1401,3 +1401,37 @@ correctness+error-handling, and performance+architecture audits across 77 source
|
|
|
1401
1401
|
|
|
1402
1402
|
### CI
|
|
1403
1403
|
- `.github/workflows/ci.yml`: typecheck step re-enabled (was disabled since v0.3.x)
|
|
1404
|
+
|
|
1405
|
+
## [0.6.0] — Source Tour Patterns Implementation (2026-06-04)
|
|
1406
|
+
|
|
1407
|
+
### Highlights
|
|
1408
|
+
- **15 patterns** implemented from 63-repo source tour (2,267 LOC)
|
|
1409
|
+
- All patterns pass TypeScript strict mode with 0 errors
|
|
1410
|
+
- 37 skills (including new council skill)
|
|
1411
|
+
|
|
1412
|
+
### Tier 1 — Quick Wins
|
|
1413
|
+
- **Council skill** (Pattern 5): 3 adversarial roles for critical decisions
|
|
1414
|
+
- **6 lifecycle hooks** (Pattern 12): after_run_complete, after_task_complete, session hooks
|
|
1415
|
+
- **3-tier convention** (Pattern 13): Command→Agent→Skill documentation + effort field
|
|
1416
|
+
- **Pre-step scripts** (Pattern 2): Deterministic scripts before LLM dispatch
|
|
1417
|
+
- **Chain DSL parser** (Pattern 8): step1 -> parallel(step2, step3) -> step4
|
|
1418
|
+
|
|
1419
|
+
### Tier 2 — Medium-Term
|
|
1420
|
+
- **DAG enhancements** (Pattern 7): findBlockedTasks, getBlockingTasks, topologicalSort
|
|
1421
|
+
- **Drift detection** (Pattern 10): 5 detectors, 2-pass reconciliation
|
|
1422
|
+
- **Hash-based task IDs** (Pattern 11): Base36 + adaptive length + hierarchical
|
|
1423
|
+
- **Iterative retrieval** (Pattern 6): Score → converge → refine loop
|
|
1424
|
+
- **Intercom bridge** (Pattern 9): Worker→orchestrator escalation queue
|
|
1425
|
+
- **Plan templates** (Pattern 15): Built-in standard-review and full-implementation
|
|
1426
|
+
|
|
1427
|
+
### Tier 3 — Long-Term
|
|
1428
|
+
- **Phase-gated intermediates** (Pattern 1): Disk-persistent step outputs
|
|
1429
|
+
- **Incremental fingerprinting** (Pattern 3): Content hash + structural signature
|
|
1430
|
+
- **4-tier memory** (Pattern 4): Working→Episodic→Semantic→Procedural with Ebbinghaus decay
|
|
1431
|
+
- **Observation system** (Pattern 14): Capture→compress→re-inject with privacy tags
|
|
1432
|
+
|
|
1433
|
+
### Stats
|
|
1434
|
+
- Test suite: 2698 pass + 1 skip, 0 fail
|
|
1435
|
+
- TypeScript: 0 errors
|
|
1436
|
+
- Skills: 37/37 PASS
|
|
1437
|
+
- New modules: 11 files, 2,267 LOC
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Command → Agent → Skill: 3-Tier Pattern
|
|
2
|
+
|
|
3
|
+
> **Origin**: `source/claude-code-best-practice/CLAUDE.md`
|
|
4
|
+
> **Applicable to**: pi-crew v0.5.25+
|
|
5
|
+
|
|
6
|
+
## The 3 Tiers
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
User invokes → [Command] → [Agent] → [Skill]
|
|
10
|
+
entry worker reusable
|
|
11
|
+
point + tools capability
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Tier 1: Command (Entry Point)
|
|
15
|
+
- Maps user intent to an agent
|
|
16
|
+
- Defined in workflow `.md` files as a step
|
|
17
|
+
- Example: `team action='run' team='review'`
|
|
18
|
+
|
|
19
|
+
### Tier 2: Agent (Specialized Worker)
|
|
20
|
+
- Has a system prompt, model, tools, skills, effort level
|
|
21
|
+
- Defined as `.md` file with YAML frontmatter in `agents/` directory
|
|
22
|
+
- Example:
|
|
23
|
+
|
|
24
|
+
```markdown
|
|
25
|
+
---
|
|
26
|
+
name: security-reviewer
|
|
27
|
+
description: Chief Security Officer who finds OWASP Top 10 threats
|
|
28
|
+
tools: read, bash, edit
|
|
29
|
+
model: claude-sonnet-4-20250514
|
|
30
|
+
effort: high
|
|
31
|
+
skills: safe-bash, security-review
|
|
32
|
+
maxTurns: 30
|
|
33
|
+
contextMode: fresh
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
You are a Chief Security Officer...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Tier 3: Skill (Reusable Capability)
|
|
40
|
+
- A `SKILL.md` file with instructions + operating rules
|
|
41
|
+
- Injected into agent context at dispatch time
|
|
42
|
+
- Example: `skills/safe-bash/SKILL.md`
|
|
43
|
+
|
|
44
|
+
## How to Compose
|
|
45
|
+
|
|
46
|
+
1. **Define the skill** — Create `skills/my-skill/SKILL.md`
|
|
47
|
+
2. **Define the agent** — Create `agents/my-agent.md` referencing the skill
|
|
48
|
+
3. **Define the workflow** — Create `workflows/my-workflow.workflow.md` referencing the agent as a step
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
# workflows/my-workflow.workflow.md
|
|
52
|
+
steps:
|
|
53
|
+
- name: analyze
|
|
54
|
+
agent: my-agent
|
|
55
|
+
# The agent loads my-skill automatically
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Agent YAML Frontmatter Reference
|
|
59
|
+
|
|
60
|
+
| Field | Type | Description |
|
|
61
|
+
|-------|------|-------------|
|
|
62
|
+
| `name` | string | Agent identifier |
|
|
63
|
+
| `description` | string | One-line description for routing |
|
|
64
|
+
| `tools` | csv | Tools the agent can use |
|
|
65
|
+
| `model` | string | Model override |
|
|
66
|
+
| `skills` | csv | Skills to inject |
|
|
67
|
+
| `effort` | `low`/`medium`/`high` | Work effort level |
|
|
68
|
+
| `maxTurns` | number | Maximum conversation turns |
|
|
69
|
+
| `contextMode` | `fresh`/`fork` | Context inheritance |
|
|
70
|
+
| `loadMode` | `essential`/`lean` | Tool loading strategy |
|
|
71
|
+
| `thinking` | string | Thinking level override |
|
package/package.json
CHANGED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: council
|
|
3
|
+
description: >
|
|
4
|
+
Spawn 3 adversarial subagents (Skeptic, Pragmatist, Critic) to evaluate a decision,
|
|
5
|
+
architecture choice, or plan. Anti-anchoring: each role receives ONLY the question,
|
|
6
|
+
not conversation history. Aggregates votes into consensus recommendation with dissent tracking.
|
|
7
|
+
Use when facing critical decisions, architecture choices, security tradeoffs, or plan reviews
|
|
8
|
+
where single-perspective analysis is insufficient.
|
|
9
|
+
origin: ECC/skills/council
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Council Pattern — Adversarial Multi-Perspective Decision Making
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
- Evaluating architecture decisions with significant tradeoffs
|
|
17
|
+
- Reviewing security-sensitive design choices
|
|
18
|
+
- Validating implementation plans before execution
|
|
19
|
+
- Resolving ambiguity where multiple valid approaches exist
|
|
20
|
+
- Deciding whether to build, buy, or extend
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
- A clearly formulated question or decision to evaluate
|
|
25
|
+
- Sufficient context about the system for meaningful analysis
|
|
26
|
+
|
|
27
|
+
## Operating Rules
|
|
28
|
+
|
|
29
|
+
1. You MUST spawn exactly 3 subagents with isolated context (fresh, not forked)
|
|
30
|
+
2. Each subagent receives ONLY the question — NO conversation history (anti-anchoring)
|
|
31
|
+
3. You MUST NOT influence any subagent's analysis direction
|
|
32
|
+
4. You MUST record all 3 votes before forming consensus
|
|
33
|
+
5. You MUST include dissent in the final recommendation
|
|
34
|
+
|
|
35
|
+
## Workflow
|
|
36
|
+
|
|
37
|
+
### Step 1: Formulate the Question
|
|
38
|
+
|
|
39
|
+
Write a clear, neutral question that includes:
|
|
40
|
+
- The decision to be made
|
|
41
|
+
- Relevant constraints (performance, security, timeline)
|
|
42
|
+
- Available options (if known)
|
|
43
|
+
- What "success" looks like
|
|
44
|
+
|
|
45
|
+
DO NOT bias the question toward any particular answer.
|
|
46
|
+
|
|
47
|
+
### Step 2: Spawn 3 Council Members
|
|
48
|
+
|
|
49
|
+
Launch 3 parallel subagents with these EXACT roles:
|
|
50
|
+
|
|
51
|
+
**Skeptic** (Goal: Find flaws):
|
|
52
|
+
```
|
|
53
|
+
You are the Skeptic on a council evaluating: [QUESTION]
|
|
54
|
+
|
|
55
|
+
Your role: Find every possible flaw, risk, and failure mode.
|
|
56
|
+
- Challenge assumptions
|
|
57
|
+
- Identify edge cases that break the proposed approach
|
|
58
|
+
- Focus on what could go WRONG
|
|
59
|
+
- Rate your confidence (0.0-1.0) and give a PRO/CON/ABSTAIN position
|
|
60
|
+
- Provide your top 3 risks
|
|
61
|
+
|
|
62
|
+
Output format:
|
|
63
|
+
Position: PRO | CON | ABSTAIN
|
|
64
|
+
Confidence: 0.0-1.0
|
|
65
|
+
Reasoning: [your analysis]
|
|
66
|
+
Top 3 Risks: [list]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Pragmatist** (Goal: Evaluate tradeoffs):
|
|
70
|
+
```
|
|
71
|
+
You are the Pragmatist on a council evaluating: [QUESTION]
|
|
72
|
+
|
|
73
|
+
Your role: Weigh practical tradeoffs objectively.
|
|
74
|
+
- Consider implementation cost, maintenance burden, team impact
|
|
75
|
+
- Evaluate time-to-value and opportunity cost
|
|
76
|
+
- Compare against realistic alternatives
|
|
77
|
+
- Rate your confidence (0.0-1.0) and give a PRO/CON/ABSTAIN position
|
|
78
|
+
|
|
79
|
+
Output format:
|
|
80
|
+
Position: PRO | CON | ABSTAIN
|
|
81
|
+
Confidence: 0.0-1.0
|
|
82
|
+
Reasoning: [your analysis]
|
|
83
|
+
Alternatives Considered: [list]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Critic** (Goal: Stress-test reasoning):
|
|
87
|
+
```
|
|
88
|
+
You are the Critic on a council evaluating: [QUESTION]
|
|
89
|
+
|
|
90
|
+
Your role: Stress-test the logical foundations of each possible answer.
|
|
91
|
+
- Identify logical fallacies in common arguments for/against
|
|
92
|
+
- Check if the question itself contains hidden assumptions
|
|
93
|
+
- Evaluate whether the stated constraints are real or assumed
|
|
94
|
+
- Rate your confidence (0.0-1.0) and give a PRO/CON/ABSTAIN position
|
|
95
|
+
|
|
96
|
+
Output format:
|
|
97
|
+
Position: PRO | CON | ABSTAIN
|
|
98
|
+
Confidence: 0.0-1.0
|
|
99
|
+
Reasoning: [your analysis]
|
|
100
|
+
Hidden Assumptions: [list]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Step 3: Aggregate Votes
|
|
104
|
+
|
|
105
|
+
Collect all 3 responses. Compute consensus:
|
|
106
|
+
|
|
107
|
+
| Vote Pattern | Consensus Level | Action |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| 3 PRO | **Strong accept** | Proceed with high confidence |
|
|
110
|
+
| 2 PRO, 1 CON | **Weak accept** | Proceed, but address CON dissent |
|
|
111
|
+
| 2 PRO, 1 ABSTAIN | **Accept with uncertainty** | Proceed, investigate ABSTAIN concerns |
|
|
112
|
+
| 1 PRO, 1 CON, 1 ABSTAIN | **No consensus** | Reformulate question or gather more data |
|
|
113
|
+
| 2 CON, 1 PRO | **Weak reject** | Do not proceed; explore alternatives |
|
|
114
|
+
| 3 CON | **Strong reject** | Reject; fundamentally rethink approach |
|
|
115
|
+
|
|
116
|
+
### Step 4: Output Recommendation
|
|
117
|
+
|
|
118
|
+
```markdown
|
|
119
|
+
## Council Decision: [Question Summary]
|
|
120
|
+
|
|
121
|
+
### Votes
|
|
122
|
+
| Role | Position | Confidence |
|
|
123
|
+
|------|----------|------------|
|
|
124
|
+
| Skeptic | PRO/CON/ABSTAIN | 0.X |
|
|
125
|
+
| Pragmatist | PRO/CON/ABSTAIN | 0.X |
|
|
126
|
+
| Critic | PRO/CON/ABSTAIN | 0.X |
|
|
127
|
+
|
|
128
|
+
### Consensus: [STRONG ACCEPT | WEAK ACCEPT | NO CONSENSUS | WEAK REJECT | STRONG REJECT]
|
|
129
|
+
|
|
130
|
+
### Recommendation
|
|
131
|
+
[One-paragraph synthesis]
|
|
132
|
+
|
|
133
|
+
### Key Insights
|
|
134
|
+
- [Best point from Skeptic]
|
|
135
|
+
- [Best point from Pragmatist]
|
|
136
|
+
- [Best point from Critic]
|
|
137
|
+
|
|
138
|
+
### Dissent
|
|
139
|
+
[Summary of any dissenting opinions and why they were overruled or remain unresolved]
|
|
140
|
+
|
|
141
|
+
### Action Items
|
|
142
|
+
- [ ] [Specific next step 1]
|
|
143
|
+
- [ ] [Specific next step 2]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Anti-Patterns
|
|
147
|
+
|
|
148
|
+
- DO NOT spawn fewer than 3 roles
|
|
149
|
+
- DO NOT share one subagent's analysis with another (contamination)
|
|
150
|
+
- DO NOT phrase the question to favor a specific outcome
|
|
151
|
+
- DO NOT override the council's consensus without documented justification
|
|
152
|
+
- DO NOT use council for trivial decisions (wastes resources)
|
|
153
|
+
|
|
154
|
+
## Enforcement — Council Gate
|
|
155
|
+
|
|
156
|
+
Before finalizing a council result, verify:
|
|
157
|
+
|
|
158
|
+
- [ ] All 3 roles spawned with isolated (fresh) context
|
|
159
|
+
- [ ] Each role received ONLY the question, no prior conversation
|
|
160
|
+
- [ ] All 3 votes recorded with confidence scores
|
|
161
|
+
- [ ] Consensus level computed from vote pattern
|
|
162
|
+
- [ ] Dissent explicitly documented (not hidden)
|
|
163
|
+
- [ ] Recommendation includes actionable next steps
|
|
@@ -36,6 +36,8 @@ export interface AgentConfig {
|
|
|
36
36
|
contextMode?: "fresh" | "fork";
|
|
37
37
|
/** Maximum turns for this agent. Overrides runtime config if set. */
|
|
38
38
|
maxTurns?: number;
|
|
39
|
+
/** Effort level for this agent. Controls how much work the agent puts in. */
|
|
40
|
+
effort?: "low" | "medium" | "high";
|
|
39
41
|
/** Tools to explicitly forbid for this agent. Takes precedence over allowedTools. */
|
|
40
42
|
disallowedTools?: string[];
|
|
41
43
|
disabled?: boolean;
|
|
@@ -376,6 +376,7 @@ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig |
|
|
|
376
376
|
defaultTools: frontmatter.defaultTools !== undefined ? parseCsv(frontmatter.defaultTools) ?? null : undefined,
|
|
377
377
|
contextMode: parseContextMode(frontmatter.contextMode),
|
|
378
378
|
maxTurns: (() => { const n = Number.parseInt(frontmatter.maxTurns, 10); return Number.isFinite(n) && n > 0 ? n : undefined; })(),
|
|
379
|
+
effort: frontmatter.effort === "low" || frontmatter.effort === "medium" || frontmatter.effort === "high" ? frontmatter.effort : undefined,
|
|
379
380
|
disabled: frontmatter.disabled === "true" || frontmatter.enabled === "false",
|
|
380
381
|
routing: triggers || useWhen || avoidWhen || cost || category ? { triggers, useWhen, avoidWhen, cost, category } : undefined,
|
|
381
382
|
};
|
package/src/hooks/types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type HookName =
|
|
2
2
|
| "before_run_start"
|
|
3
|
+
| "after_run_complete"
|
|
3
4
|
| "before_task_start"
|
|
5
|
+
| "after_task_complete"
|
|
4
6
|
| "task_result"
|
|
5
7
|
| "before_cancel"
|
|
6
8
|
| "before_retry"
|
|
@@ -8,8 +10,20 @@ export type HookName =
|
|
|
8
10
|
| "before_cleanup"
|
|
9
11
|
| "before_publish"
|
|
10
12
|
| "session_before_switch"
|
|
13
|
+
| "session_after_connect"
|
|
14
|
+
| "session_after_disconnect"
|
|
11
15
|
| "run_recovery";
|
|
12
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Hook exit codes inspired by claude-mem's lifecycle architecture:
|
|
19
|
+
* - 0 = allow (success)
|
|
20
|
+
* - 1 = warn (non-blocking error, continue)
|
|
21
|
+
* - 2 = block (blocking error, stop)
|
|
22
|
+
*/
|
|
23
|
+
export const HOOK_EXIT_SUCCESS = 0 as const;
|
|
24
|
+
export const HOOK_EXIT_WARN = 1 as const;
|
|
25
|
+
export const HOOK_EXIT_BLOCK = 2 as const;
|
|
26
|
+
|
|
13
27
|
export type HookMode = "blocking" | "non_blocking";
|
|
14
28
|
export type HookOutcome = "allow" | "block" | "modify" | "diagnostic";
|
|
15
29
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chain/Parallel DSL Parser — parses workflow chain expressions.
|
|
3
|
+
*
|
|
4
|
+
* Syntax:
|
|
5
|
+
* step1 -> step2 -> parallel(step3, step4) -> step5
|
|
6
|
+
* step1:3 -> step2 --with-context -> step3
|
|
7
|
+
* parallel(a, b, parallel(c, d)) -> e
|
|
8
|
+
*
|
|
9
|
+
* Pattern origin: pi-prompt-template-model chain-parser.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface ChainStep {
|
|
13
|
+
/** Step name (maps to agent or workflow step ID) */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Nested parallel group */
|
|
16
|
+
parallel?: ChainStep[];
|
|
17
|
+
/** Loop count (default: 1) */
|
|
18
|
+
loopCount?: number;
|
|
19
|
+
/** Pass predecessor output as context */
|
|
20
|
+
withContext?: boolean;
|
|
21
|
+
/** Positional arguments */
|
|
22
|
+
args?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a chain DSL string into an AST.
|
|
27
|
+
*
|
|
28
|
+
* @throws {Error} on syntax errors (unclosed parens, empty names, etc.)
|
|
29
|
+
*/
|
|
30
|
+
export function parseChainDSL(input: string): ChainStep[] {
|
|
31
|
+
const tokens = tokenize(input);
|
|
32
|
+
const parser = new ChainParser(tokens);
|
|
33
|
+
return parser.parse();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Tokenizer ────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
type TokenType = "NAME" | "ARROW" | "LPAREN" | "RPAREN" | "COMMA" | "COLON" | "NUMBER" | "FLAG" | "QUOTED";
|
|
39
|
+
|
|
40
|
+
interface Token {
|
|
41
|
+
type: TokenType;
|
|
42
|
+
value: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function tokenize(input: string): Token[] {
|
|
46
|
+
const tokens: Token[] = [];
|
|
47
|
+
let i = 0;
|
|
48
|
+
|
|
49
|
+
while (i < input.length) {
|
|
50
|
+
// Skip whitespace
|
|
51
|
+
if (/\s/.test(input[i]!)) { i++; continue; }
|
|
52
|
+
|
|
53
|
+
// Arrow ->
|
|
54
|
+
if (input[i] === "-" && input[i + 1] === ">") {
|
|
55
|
+
tokens.push({ type: "ARROW", value: "->" });
|
|
56
|
+
i += 2; continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Punctuation
|
|
60
|
+
if (input[i] === "(") { tokens.push({ type: "LPAREN", value: "(" }); i++; continue; }
|
|
61
|
+
if (input[i] === ")") { tokens.push({ type: "RPAREN", value: ")" }); i++; continue; }
|
|
62
|
+
if (input[i] === ",") { tokens.push({ type: "COMMA", value: "," }); i++; continue; }
|
|
63
|
+
if (input[i] === ":") { tokens.push({ type: "COLON", value: ":" }); i++; continue; }
|
|
64
|
+
|
|
65
|
+
// Quoted argument
|
|
66
|
+
if (input[i] === '"' || input[i] === "'") {
|
|
67
|
+
const quote = input[i];
|
|
68
|
+
let str = "";
|
|
69
|
+
i++; // skip opening quote
|
|
70
|
+
while (i < input.length && input[i] !== quote) {
|
|
71
|
+
if (input[i] === "\\" && i + 1 < input.length) { i++; str += input[i]; }
|
|
72
|
+
else { str += input[i]!; }
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
if (i >= input.length) throw new Error("Unclosed quoted string in chain DSL");
|
|
76
|
+
i++; // skip closing quote
|
|
77
|
+
tokens.push({ type: "QUOTED", value: str });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Flag --with-context
|
|
82
|
+
if (input[i] === "-" && input[i + 1] === "-") {
|
|
83
|
+
let flag = "";
|
|
84
|
+
i += 2;
|
|
85
|
+
while (i < input.length && /[a-zA-Z0-9_-]/.test(input[i]!)) { flag += input[i]; i++; }
|
|
86
|
+
tokens.push({ type: "FLAG", value: flag });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Number
|
|
91
|
+
if (/[0-9]/.test(input[i]!)) {
|
|
92
|
+
let num = "";
|
|
93
|
+
while (i < input.length && /[0-9]/.test(input[i]!)) { num += input[i]; i++; }
|
|
94
|
+
tokens.push({ type: "NUMBER", value: num });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Name
|
|
99
|
+
if (/[a-zA-Z_]/.test(input[i]!)) {
|
|
100
|
+
let name = "";
|
|
101
|
+
while (i < input.length && /[a-zA-Z0-9_.-]/.test(input[i]!)) { name += input[i]; i++; }
|
|
102
|
+
tokens.push({ type: "NAME", value: name });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(`Unexpected character '${input[i]}' at position ${i} in chain DSL`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return tokens;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Recursive Descent Parser ─────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
class ChainParser {
|
|
115
|
+
private pos = 0;
|
|
116
|
+
|
|
117
|
+
constructor(private tokens: Token[]) {}
|
|
118
|
+
|
|
119
|
+
parse(): ChainStep[] {
|
|
120
|
+
const steps: ChainStep[] = [];
|
|
121
|
+
steps.push(this.parseStep());
|
|
122
|
+
while (this.peek("ARROW")) {
|
|
123
|
+
this.consume("ARROW");
|
|
124
|
+
steps.push(this.parseStep());
|
|
125
|
+
}
|
|
126
|
+
if (this.pos < this.tokens.length) {
|
|
127
|
+
throw new Error(`Unexpected token '${this.tokens[this.pos]?.value}' at position ${this.pos}`);
|
|
128
|
+
}
|
|
129
|
+
return steps;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private parseStep(): ChainStep {
|
|
133
|
+
// Check for parallel(...) construct
|
|
134
|
+
if (this.peek("NAME", "parallel")) {
|
|
135
|
+
this.consume("NAME"); // eat "parallel"
|
|
136
|
+
this.consume("LPAREN");
|
|
137
|
+
const parallel: ChainStep[] = [];
|
|
138
|
+
parallel.push(this.parseStep());
|
|
139
|
+
while (this.peek("COMMA")) {
|
|
140
|
+
this.consume("COMMA");
|
|
141
|
+
parallel.push(this.parseStep());
|
|
142
|
+
}
|
|
143
|
+
this.consume("RPAREN");
|
|
144
|
+
const step: ChainStep = { name: "parallel", parallel };
|
|
145
|
+
this.parseModifiers(step);
|
|
146
|
+
return step;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Normal step name
|
|
150
|
+
const name = this.consume("NAME").value;
|
|
151
|
+
const step: ChainStep = { name };
|
|
152
|
+
|
|
153
|
+
// Parse modifiers
|
|
154
|
+
this.parseModifiers(step);
|
|
155
|
+
return step;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private parseModifiers(step: ChainStep): void {
|
|
159
|
+
while (this.pos < this.tokens.length) {
|
|
160
|
+
if (this.peek("COLON")) {
|
|
161
|
+
this.consume("COLON");
|
|
162
|
+
const num = this.consume("NUMBER");
|
|
163
|
+
step.loopCount = Number.parseInt(num.value, 10);
|
|
164
|
+
} else if (this.peek("FLAG", "with-context")) {
|
|
165
|
+
this.consume("FLAG");
|
|
166
|
+
step.withContext = true;
|
|
167
|
+
} else if (this.peek("QUOTED")) {
|
|
168
|
+
const arg = this.consume("QUOTED");
|
|
169
|
+
step.args = step.args ?? [];
|
|
170
|
+
step.args.push(arg.value);
|
|
171
|
+
} else {
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private peek(type: TokenType, value?: string): boolean {
|
|
178
|
+
const tok = this.tokens[this.pos];
|
|
179
|
+
if (!tok) return false;
|
|
180
|
+
if (tok.type !== type) return false;
|
|
181
|
+
if (value !== undefined && tok.value !== value) return false;
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private consume(type: TokenType): Token {
|
|
186
|
+
const tok = this.tokens[this.pos];
|
|
187
|
+
if (!tok) throw new Error(`Expected ${type} but reached end of chain DSL`);
|
|
188
|
+
if (tok.type !== type) throw new Error(`Expected ${type} but got ${tok.type}('${tok.value}')`);
|
|
189
|
+
this.pos++;
|
|
190
|
+
return tok;
|
|
191
|
+
}
|
|
192
|
+
}
|