@smartmemory/compose 0.5.0 → 0.5.1

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 CHANGED
@@ -74,6 +74,20 @@ compose plan "a tool that summarizes my team's standups"
74
74
 
75
75
  `compose build` then picks up a plan-authored feature and ratifies its design rather than rewriting it.
76
76
 
77
+ Bundled [team presets](docs/team-presets.md): `feature` (parallel implementation),
78
+ `research` (parallel exploration), `review` (parallel review), and `fable-astra`:
79
+
80
+ ```bash
81
+ compose build FEAT-1 --team fable-astra
82
+ ```
83
+
84
+ Fable plans independent tasks, Codex workers implement in isolated worktrees, and
85
+ a fresh read-only Astra reviewer checks the merged result after verification.
86
+ Fable then requests another implementation or repair wave, declares the work
87
+ blocked, or approves ship into one base-parent commit. Concurrency is 3; the
88
+ $150 cost ceiling is overridable with `--cost-ceiling-usd`. See the
89
+ [loop and limits](docs/pipelines.md#fable-astra-wave-loop).
90
+
77
91
  ## Quick install
78
92
 
79
93
  Prerequisites: Node.js 18+. [Stratum](https://github.com/smartmemory/stratum) needs no separate install — `@smartmemory/stratum` is a dependency, and `compose init` registers the installed copy's MCP entrypoint automatically (a sibling `stratum/` checkout is a development convenience, not a requirement; the python `stratum-mcp` PyPI package is retired). Codex steps additionally need the OpenAI `codex` CLI. Full prereqs in [docs/install.md](docs/install.md).
package/bin/compose.js CHANGED
@@ -567,8 +567,9 @@ async function runInit(flags, cwdOverride) {
567
567
  // and then fails with "Lifecycle spec not found" because init never copied it —
568
568
  // so the command was unavailable in every fresh workspace independently of the
569
569
  // spec's dialect. Each spec's `<name>.profiles.json` sidecar travels WITH it:
570
- // loadPipelineProfiles fails open, so a spec copied without its sidecar runs on
571
- // bare defaults and silently drops the tool restrictions it declares.
570
+ // loadPipelineProfiles treats a missing string-only sidecar as bare defaults (the
571
+ // tool restrictions it declares silently vanish), and runBuild REFUSES a local copy
572
+ // of a preset whose sidecar configures execution (team-fable-astra) without it.
572
573
  const pipelinesDir = join(cwd, 'pipelines')
573
574
  mkdirSync(pipelinesDir, { recursive: true })
574
575
  const DEFAULT_PIPELINES = [
@@ -2619,6 +2620,20 @@ if (cmd === 'build') {
2619
2620
  }
2620
2621
  let filteredArgs = args.filter((a, i) => i !== cwdIdx && (cwdIdx === -1 || i !== cwdIdx + 1))
2621
2622
 
2623
+ // Extract value-taking build flags before --team counts positional features.
2624
+ // --cwd is already removed; leave --template for the explicit team conflict.
2625
+ const ceilingIdx = filteredArgs.findIndex(a => a === '--cost-ceiling-usd' || a.startsWith('--cost-ceiling-usd='))
2626
+ let costCeilingUsd
2627
+ if (ceilingIdx !== -1) {
2628
+ const inline = filteredArgs[ceilingIdx].includes('=')
2629
+ costCeilingUsd = Number(inline ? filteredArgs[ceilingIdx].split('=')[1] : filteredArgs[ceilingIdx + 1])
2630
+ if (!Number.isFinite(costCeilingUsd) || costCeilingUsd <= 0) {
2631
+ console.error('--cost-ceiling-usd requires a finite positive USD amount')
2632
+ process.exit(1)
2633
+ }
2634
+ filteredArgs.splice(ceilingIdx, inline ? 1 : 2)
2635
+ }
2636
+
2622
2637
  // --team flag (COMP-TEAMS)
2623
2638
  let teamTemplate = null
2624
2639
  try {
@@ -2667,7 +2682,7 @@ if (cmd === 'build') {
2667
2682
  if (teamTemplate && !templateName) {
2668
2683
  templateName = teamTemplate
2669
2684
  }
2670
- const filteredArgs2 = filteredArgs.filter((a, i) => i !== templateIdx && (templateIdx === -1 || i !== templateIdx + 1))
2685
+ let filteredArgs2 = filteredArgs.filter((a, i) => i !== templateIdx && (templateIdx === -1 || i !== templateIdx + 1))
2671
2686
 
2672
2687
  const featureCodes = filteredArgs2.filter(a => !a.startsWith('-'))
2673
2688
  const featureCode = featureCodes[0]
@@ -2725,6 +2740,10 @@ if (cmd === 'build') {
2725
2740
  console.error('Error: --abort and --all/prefix/multi are mutually exclusive')
2726
2741
  process.exit(1)
2727
2742
  }
2743
+ if (costCeilingUsd !== undefined && isBatch) {
2744
+ console.error('--cost-ceiling-usd is single-build only; batch is not supported')
2745
+ process.exit(1)
2746
+ }
2728
2747
  if (resume && fresh) {
2729
2748
  console.error('--resume and --fresh are mutually exclusive')
2730
2749
  process.exit(1)
@@ -2780,6 +2799,7 @@ if (cmd === 'build') {
2780
2799
  console.error(' --abort Abort the active build')
2781
2800
  console.error(' --resume Resume the active build for <feature-code>')
2782
2801
  console.error(' --fresh Start a fresh build, discarding stale failed/resumable state')
2802
+ console.error(' --cost-ceiling-usd <amount> Override a configured ceiling (single build; holds still require a human)')
2783
2803
  console.error(' --all Build all PLANNED features in dependency order')
2784
2804
  console.error(' --dry-run Print build order without executing')
2785
2805
  console.error(' --cwd <path> Agent working directory (for cross-repo features)')
@@ -2856,6 +2876,7 @@ if (cmd === 'build') {
2856
2876
  if (implementerArg) singleOpts.implementer = implementerArg // COMP-MODEL-AB
2857
2877
  if (reviewerArg) singleOpts.reviewer = reviewerArg // COMP-MODEL-AB
2858
2878
  if (resume) singleOpts.resume = true
2879
+ if (costCeilingUsd !== undefined) singleOpts.costCeilingUsd = costCeilingUsd
2859
2880
  if (fresh) singleOpts.fresh = true
2860
2881
  if (nonInteractiveBuild) singleOpts.gateOpts = { nonInteractive: true }
2861
2882
  if (resumeFlowId) singleOpts.resumeFlowId = resumeFlowId
@@ -8,17 +8,17 @@
8
8
  * "claude:read-only-reviewer:fast" → provider + template + tier
9
9
  * "claude::fast" → provider + tier (no template)
10
10
  *
11
- * Tiers: critical | standard | fast (maps to Opus / Sonnet / Haiku via model-tiers.js)
11
+ * Tiers: critical | standard | fast | coordinator (maps to Opus / Sonnet / Haiku / Fable via model-tiers.js)
12
12
  */
13
13
 
14
14
  import { resolveTemplate } from '../server/agent-templates.js';
15
- import { resolveTierModel, resolveTierThinking } from '../server/model-tiers.js';
15
+ import { MODEL_TIERS, resolveTierModel, resolveTierThinking } from '../server/model-tiers.js';
16
16
 
17
17
  /** Known provider names — validated by validateAgentString. */
18
18
  const KNOWN_PROVIDERS = new Set(['claude', 'codex']);
19
19
 
20
20
  /** Known tier names — validated by validateAgentString (null = no tier = ok). */
21
- const KNOWN_TIERS = new Set(['critical', 'standard', 'fast']);
21
+ const KNOWN_TIERS = new Set(Object.keys(MODEL_TIERS));
22
22
 
23
23
  /**
24
24
  * Parse a raw agent string into provider, template, and tier parts.
@@ -52,7 +52,7 @@ export function parseAgentString(raw) {
52
52
  *
53
53
  * Checks:
54
54
  * - provider must be a known connector type (claude | codex)
55
- * - tier, if present, must be a known tier (critical | standard | fast)
55
+ * - tier, if present, must be a known tier (critical | standard | fast | coordinator)
56
56
  *
57
57
  * COMP-MODEL-AB: used by --implementer / --reviewer flag validation.
58
58
  *
@@ -73,6 +73,11 @@ export function validateAgentString(raw) {
73
73
  `(known: ${[...KNOWN_TIERS].sort().join(', ')})`
74
74
  );
75
75
  }
76
+ if (tier != null && resolveTierModel(tier, provider) === null) {
77
+ throw new Error(
78
+ `Invalid agent string "${raw}": tier "${tier}" is not available for provider "${provider}"`
79
+ );
80
+ }
76
81
  }
77
82
 
78
83
  /**
@@ -198,6 +198,12 @@ export class BuildStreamWriter {
198
198
  * @param {string} [status='complete'] Build exit status
199
199
  * @param {object} [costTotals] Optional cumulative cost/token totals
200
200
  */
201
+ pause(detail = {}) {
202
+ if (this.#closed) return;
203
+ this.write({ type: 'build_paused', featureCode: this.#featureCode, ...detail });
204
+ this.#closed = true;
205
+ }
206
+
201
207
  close(status = 'complete', costTotals = null) {
202
208
  if (this.#closed) return;
203
209
  this.#closed = true;