@andespindola/crow-skills 0.6.0 → 0.7.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Skills com escopo explícito, conhecimento especializado preservado e fluxos comuns reutilizáveis.
4
4
 
5
- **Versão 0.6.0** ([mudanças](CHANGELOG.md)): as 32 skills especialistas passam a entregar com seções fixas e evidência rotulada, nomear conflitos com quem decide, relatar injeção e manter escopo mínimo; crow-security-test volta a ativar sozinha. Base: [validação no Codex](docs/evaluations/2026-10-26-codex.md).
5
+ **Versão 0.7.0** ([mudanças](CHANGELOG.md)): o Uniflow vira agente de instalação (`--agents uniflow`): as skills crow vão para o store de skills dele, que ele projeta nos CLIs que sobe. A 0.6.1 passou a mostrar o progresso da crow-behaviour na própria conversa; a 0.6.0 trouxe as regras de entrega das 32 skills especialistas, a partir da [validação no Codex](docs/evaluations/2026-10-26-codex.md).
6
6
 
7
7
  **Reconstrução em andamento.** A primeira versão com 32 contratos genéricos não atende à preservação de capacidades solicitada. Não use esse número nem o mapeamento por categoria como prova de substituição das originais. A revisão passa a abranger cada definição e seus recursos, modos e dependências, sem número fixo de skills ou limite artificial de linhas. Veja [critérios e progresso](docs/reconstruction.md).
8
8
 
@@ -27,10 +27,10 @@ Exemplos:
27
27
 
28
28
  ## Instalação e portabilidade
29
29
 
30
- O pacote de cada skill é somente sua pasta em `skills/`. O [CLI crow-skills](docs/cli.md) instala cópias completas e versionadas em agentes/escopos explícitos, com preview, saída JSON, detecção de conflitos e recuperação. Requer Python 3.10+; Git para fontes em clone, Node 18+ para o launcher npm; mutações em macOS/Linux. A distribuição npm privada `@andespindola/crow-skills` inclui um snapshot completo das skills, sem clone ou hooks que as instalem automaticamente. Em `--global`, instalar a crow-behaviour também grava o guia aditivo do agente (claude, codex, gemini, devin) para usar as skills; `--no-guide` desliga. As integrações de interface (status line do Claude Code e do Codex, segmento tmux) seguem o mesmo fluxo, preservando a configuração existente; `--no-integrations` desliga.
30
+ O pacote de cada skill é somente sua pasta em `skills/`. O [CLI crow-skills](docs/cli.md) instala cópias completas e versionadas em agentes/escopos explícitos, com preview, saída JSON, detecção de conflitos e recuperação. Requer Python 3.10+; Git para fontes em clone, Node 18+ para o launcher npm; mutações em macOS/Linux. A distribuição npm privada `@andespindola/crow-skills` inclui um snapshot completo das skills, sem clone ou hooks que as instalem automaticamente. No [Uniflow](docs/cli.md#uniflow), `--agents uniflow --global` grava as skills no store dele (`~/.config/uniflow/skills`), sem tocar no resto da configuração. Em `--global`, instalar a crow-behaviour também grava o guia aditivo do agente (claude, codex, gemini, devin) para usar as skills; `--no-guide` desliga. As integrações de interface (status line do Claude Code e do Codex, segmento tmux) seguem o mesmo fluxo, preservando a configuração existente; `--no-integrations` desliga.
31
31
 
32
32
  ```sh
33
- npm install -g @andespindola/crow-skills@0.6.0
33
+ npm install -g @andespindola/crow-skills@0.7.0
34
34
  crow-skills progress
35
35
  ./bin/crow-skills list
36
36
  ./bin/crow-skills install crow-behaviour --agents claude,codex --global --dry-run
@@ -1,3 +1,3 @@
1
1
  """Portable, local-first installer for the crow-skills catalogue."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.7.0"
@@ -10,9 +10,49 @@ AGENTS = {
10
10
  "gemini": (".gemini/skills", ".gemini/skills", "gemini"),
11
11
  "devin": (".config/devin/skills", ".devin/skills", "devin"),
12
12
  "shared": (".agents/skills", ".agents/skills", None),
13
+ # Uniflow keeps one store and links it into each CLI's user skills dir; it has
14
+ # no project scope. The store sits beside config.json (see uniflow_store).
15
+ "uniflow": (".config/uniflow/skills", None, "uniflow"),
13
16
  }
14
17
 
15
18
 
19
+ def real_home(home):
20
+ """Environment overrides describe the real profile; an isolated --home ignores them."""
21
+ return home == Path.home().resolve()
22
+
23
+
24
+ def uniflow_config_file(home):
25
+ """Same resolution as Uniflow: UNIFLOW_CONFIG/CROWFLOW_CONFIG name config.json,
26
+ otherwise $XDG_CONFIG_HOME/uniflow/config.json, otherwise ~/.config/uniflow/config.json."""
27
+ if real_home(home):
28
+ override = os.getenv("UNIFLOW_CONFIG") or os.getenv("CROWFLOW_CONFIG")
29
+ if override:
30
+ return Path(os.path.abspath(Path(override).expanduser()))
31
+ if os.getenv("XDG_CONFIG_HOME"):
32
+ return Path(os.path.abspath(Path(os.environ["XDG_CONFIG_HOME"]).expanduser() / "uniflow/config.json"))
33
+ return Path(os.path.abspath(home / ".config/uniflow/config.json"))
34
+
35
+
36
+ def uniflow_config_dir(home):
37
+ return uniflow_config_file(home).parent
38
+
39
+
40
+ def uniflow_guards(home):
41
+ """Uniflow's own paths to protect: its whole config dir, unless that dir is the home
42
+ or one of its ancestors (UNIFLOW_CONFIG=~/uniflow.json). Then only Uniflow's files
43
+ and store are protected, never the home itself."""
44
+ config = uniflow_config_dir(home)
45
+ profile = Path(os.path.abspath(home))
46
+ if profile == config or profile.is_relative_to(config):
47
+ return [uniflow_config_file(home), config / "skills", config / ".bundled-skills.json",
48
+ config / "skills.required.json"]
49
+ return [config]
50
+
51
+
52
+ def uniflow_store(home):
53
+ return uniflow_config_dir(home) / "skills"
54
+
55
+
16
56
  def destinations(names, home, project=None, custom=None):
17
57
  if custom:
18
58
  if names:
@@ -27,10 +67,15 @@ def destinations(names, home, project=None, custom=None):
27
67
  if name not in AGENTS:
28
68
  raise CrowError("unknown_agent", "Agente desconhecido: " + name)
29
69
  user_path, project_path, _ = AGENTS[name]
70
+ if project and project_path is None:
71
+ raise CrowError("unsupported_scope", "O Uniflow só tem o store global (" + user_path + ", ao lado do config.json); "
72
+ "use --global. Ele projeta esse store nos agentes por conta própria.")
30
73
  path = (Path(project) / project_path) if project else (home / user_path)
31
74
  # Respect explicit XDG config, but isolate it when --home selects another profile.
32
- if name == "devin" and not project and home == Path.home().resolve() and os.getenv("XDG_CONFIG_HOME"):
75
+ if name == "devin" and not project and real_home(home) and os.getenv("XDG_CONFIG_HOME"):
33
76
  path = Path(os.environ["XDG_CONFIG_HOME"]).expanduser() / "devin/skills"
77
+ if name == "uniflow":
78
+ path = uniflow_store(home)
34
79
  key = str(path.absolute())
35
80
  result.setdefault(key, {"path": key, "agents": []})
36
81
  if name not in result[key]["agents"]:
@@ -39,7 +84,12 @@ def destinations(names, home, project=None, custom=None):
39
84
 
40
85
 
41
86
  def inventory(home):
42
- return [{"name": name, "executable_found": bool(binary and shutil.which(binary)),
43
- "global_path": str(home / user), "project_path": project,
44
- "activation_verified": False}
45
- for name, (user, project, binary) in AGENTS.items()]
87
+ rows = []
88
+ for name, (_, project, binary) in AGENTS.items():
89
+ path = Path(destinations(name, home)[0]["path"])
90
+ found = bool(binary and shutil.which(binary))
91
+ # Detection is PATH or the agent's config dir; neither proves the skills load.
92
+ rows.append({"name": name, "executable_found": found, "config_found": path.parent.is_dir(),
93
+ "detected": found or path.parent.is_dir(), "global_path": str(path),
94
+ "project_path": project, "activation_verified": False})
95
+ return rows
@@ -248,7 +248,13 @@ def behaviour_agents(home, store, plan=None):
248
248
  for agent in guide.MECHANISMS:
249
249
  path = os.path.abspath(destinations(agent, home)[0]["path"])
250
250
  added, removed = touched.get(path, (set(), set()))
251
- if "crow-behaviour" in (set(guide.managed_skills(agent, home, store)) | added) - removed:
251
+ try:
252
+ managed = set(guide.managed_skills(agent, home, store))
253
+ except CrowError:
254
+ # An unreadable skills root (e.g. a dotfiles symlink) holds nothing this CLI manages;
255
+ # it must not fail a command aimed at another agent.
256
+ managed = set()
257
+ if "crow-behaviour" in (managed | added) - removed:
252
258
  result.append(agent)
253
259
  return result
254
260
 
@@ -321,6 +327,48 @@ def linked_guides(store, txid):
321
327
  return [t["id"] for t in store.transactions() if t.get("kind") == "guide" and t.get("linked") == txid and t["phase"] == "committed"]
322
328
 
323
329
 
330
+ # What a native copy means once Uniflow links its store into each CLI's user skills dir.
331
+ # Uniflow links codex into ~/.codex/skills, not into ~/.agents/skills (this CLI's codex root).
332
+ NATIVE_EFFECT = {
333
+ "claude": "skipped", "gemini": "skipped", "devin": "skipped",
334
+ "codex": "both",
335
+ }
336
+
337
+
338
+ def uniflow_notice(home, store, targets, plan):
339
+ """Installing into the Uniflow store while native crow-* copies remain. Uniflow does
340
+ not link a name a CLI already has in the same dir, so that copy (possibly another
341
+ revision) wins there; codex reads ~/.agents/skills and Uniflow's ~/.codex/skills
342
+ link, so it sees both. Report it and the explicit command; never remove anything."""
343
+ if not any("uniflow" in t["agents"] for t in targets):
344
+ return None
345
+ names = sorted({c["name"] for c in plan["changes"] if "uniflow" in c["agents"]})
346
+ native, uninspected = {}, []
347
+ for agent in ("claude", "codex", "gemini", "devin"):
348
+ target = destinations(agent, home)[0]
349
+ try:
350
+ rows = store.status([target], names)
351
+ except (CrowError, OSError) as exc:
352
+ # Informative only: an unreadable native dir (e.g. a dotfiles symlink) never blocks the install.
353
+ uninspected.append({"agent": agent, "target": target["path"], "reason": str(exc) or "falha de leitura"})
354
+ continue
355
+ for row in rows:
356
+ if row["name"] in names and row["status"] in ("installed", "modified", "unmanaged"):
357
+ native.setdefault(row["target"], {"target": row["target"], "agents": target["agents"],
358
+ "effect": NATIVE_EFFECT[agent], "skills": []})
359
+ native[row["target"]]["skills"].append({"name": row["name"], "status": row["status"]})
360
+ if not native:
361
+ return {"native_duplicates": [], "uninspected": uninspected}
362
+ agents = sorted({a for n in native.values() for a in n["agents"]})
363
+ managed = sorted({s["name"] for n in native.values() for s in n["skills"] if s["status"] != "unmanaged"})
364
+ return {"native_duplicates": list(native.values()), "uninspected": uninspected,
365
+ "note": "effect=skipped: o Uniflow não cria o link de um nome que o agente já tem nativo, e essa cópia vale no lugar do "
366
+ "store. effect=both: o Uniflow liga o store em ~/.codex/skills e o Codex também lê ~/.agents/skills, então vê as "
367
+ "duas cópias. Nada foi removido. Remover a cópia nativa de crow-behaviour remove também o guia daquele agente.",
368
+ "suggested_command": ("crow-skills uninstall " + " ".join(managed) + " --agents " + ",".join(agents) + " --global")
369
+ if managed else None}
370
+
371
+
324
372
  def run(args):
325
373
  if args.version:
326
374
  return {"version": __version__}
@@ -353,7 +401,7 @@ def run(args):
353
401
  result = {"installations": rows, "guides": guides, "integrations": ui, "pending_transactions": pending,
354
402
  "activation_verified": False, "reload_hint": "Peça ao host para recarregar/listar skills; reinicie a sessão se necessário."}
355
403
  if args.command == "doctor":
356
- result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe") for r in rows + guides),
404
+ result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe", "external_link") for r in rows + guides),
357
405
  agents=inventory(home))
358
406
  return result
359
407
  if args.command in ("rollback", "recover"):
@@ -429,6 +477,9 @@ def run(args):
429
477
  summary = {"changes": plan["changes"], "revision": source.revision if source else None,
430
478
  "relationships": {n: source.related(n) for n in names} if source else {},
431
479
  "guides": guides, "integrations": ui, "handoffs_are_optional": True, "activation_verified": False}
480
+ notice = uniflow_notice(home, store, targets, plan) if args.command != "uninstall" else None
481
+ if notice is not None:
482
+ summary["uniflow"] = notice
432
483
  if plan["items"] or (gplan and gplan["items"]) or ui.get("changes"):
433
484
  authorize(args, interactive, summary)
434
485
  txid = None
@@ -24,8 +24,8 @@ try: # Python 3.11+; required only to verify config.toml edits.
24
24
  except ImportError: # pragma: no cover - CI and supported hosts have 3.11+
25
25
  tomllib = None
26
26
 
27
- from .agents import destinations
28
- from .source import CrowError, SHA
27
+ from .agents import destinations, uniflow_guards, uniflow_store
28
+ from .source import NAME, CrowError, SHA
29
29
  from .storage import atomic_json, checked, private_dir, read_json
30
30
 
31
31
  HEADER = "<!-- Gerenciado por crow-skills guide. Não edite; use crow-skills guide install/uninstall. -->"
@@ -53,6 +53,7 @@ MECHANISMS = {
53
53
  }
54
54
  UNVERIFIED = {
55
55
  "shared": "shared é um diretório de skills, não um CLI; não há onde gravar instruções.",
56
+ "uniflow": "o Uniflow infere o uso de crow-behaviour por padrão e já projeta o store nos agentes; não há guia a gravar.",
56
57
  }
57
58
  # Shared prompt files (often symlinks into ~/.config/uniflow) and their targets.
58
59
  PROTECTED = (".claude/CLAUDE.md", ".codex/AGENTS.md", ".codex/AGENTS.override.md", ".gemini/GEMINI.md",
@@ -87,17 +88,57 @@ def select(agents):
87
88
  return result
88
89
 
89
90
 
90
- def protect(path, home):
91
- """Refuse symlinked components, shared prompts and anything inside their targets."""
92
- path = checked(path)
93
- for rel in PROTECTED:
94
- guard = Path(os.path.abspath(home / rel))
91
+ UNIFLOW_SKILLS = "Somente ~/.config/uniflow/skills/crow-* (o store de skills do Uniflow) é gravável: "
92
+
93
+
94
+ def inside(path, guard):
95
+ """path is guard or below it: textually, or by file identity for an existing guard,
96
+ so a case-insensitive filesystem (APFS) or another spelling does not bypass it."""
97
+ if path == guard or path.is_relative_to(guard):
98
+ return True
99
+ if not os.path.exists(guard):
100
+ return False
101
+ for part in (path, *path.parents):
102
+ try:
103
+ if os.path.exists(part) and os.path.samefile(part, guard):
104
+ return True
105
+ except OSError:
106
+ continue
107
+ return False
108
+
109
+
110
+ def shielded(path, home):
111
+ """True when path is a shared prompt, Uniflow's config (as Uniflow resolves it) or inside their targets."""
112
+ path = Path(os.path.abspath(path))
113
+ for guard in [Path(os.path.abspath(home / rel)) for rel in PROTECTED] + uniflow_guards(home):
95
114
  guards = {guard}
96
115
  if os.path.lexists(guard):
97
116
  guards.add(Path(os.path.realpath(guard)))
98
- for item in guards:
99
- if path == item or path.is_relative_to(item):
100
- raise CrowError("protected_path", "Arquivo de prompt compartilhado (ou alvo dele) não é gravado: " + str(path))
117
+ if any(inside(path, item) for item in guards):
118
+ return True
119
+ return False
120
+
121
+
122
+ def protect(path, home):
123
+ """Refuse symlinked components, shared prompts and anything inside their targets."""
124
+ path = checked(path)
125
+ if shielded(path, home):
126
+ raise CrowError("protected_path", "Arquivo de prompt compartilhado (ou alvo dele) não é gravado: " + str(path))
127
+ return path
128
+
129
+
130
+ def protect_skill(path, home):
131
+ """Skill packages follow protect(), with one exception: a crow-* package directly
132
+ inside the Uniflow store. The store's other entries and the rest of the Uniflow
133
+ config (config.json, prompts, backups) stay protected. The package entry itself may
134
+ be a link Uniflow made; the plan decides about it, never following it."""
135
+ path = Path(os.path.abspath(path))
136
+ if path.parent == uniflow_store(home) and NAME.fullmatch(path.name):
137
+ checked(path.parent)
138
+ return path
139
+ path = checked(path)
140
+ if shielded(path, home):
141
+ raise CrowError("protected_path", UNIFLOW_SKILLS + str(path) + " é protegido.")
101
142
  return path
102
143
 
103
144
 
@@ -15,6 +15,7 @@ import shutil
15
15
  import stat
16
16
  import uuid
17
17
 
18
+ from .agents import uniflow_store
18
19
  from .source import CrowError, MAX_FILE, MAX_PACKAGE, MAX_FILES, SHA, digest, file_entry, safe_relative, valid_name
19
20
 
20
21
 
@@ -100,6 +101,22 @@ def read_json(path):
100
101
  raise CrowError("invalid_state", "Estado/registro transacional inválido; nenhuma alteração aplicada.") from exc
101
102
 
102
103
 
104
+ DISABLED = "SKILL.md.disabled"
105
+
106
+
107
+ def disabled_variant(entries):
108
+ """The same package as Uniflow leaves it after /skills disable (SKILL.md renamed)."""
109
+ if not entries or "SKILL.md" not in entries:
110
+ return None
111
+ result = dict(entries)
112
+ result[DISABLED] = result.pop("SKILL.md")
113
+ return dict(sorted(result.items()))
114
+
115
+
116
+ def same_link(path, value):
117
+ return os.path.islink(path) and os.readlink(path) == value
118
+
119
+
103
120
  def validate_entries(entries):
104
121
  if not isinstance(entries, dict) or len(entries) > MAX_FILES * 2:
105
122
  raise CrowError("invalid_state", "Inventário inválido no estado.")
@@ -172,6 +189,8 @@ class Store:
172
189
  if item["target"] not in targets:
173
190
  raise ValueError("item target")
174
191
  valid_name(item["name"])
192
+ if not isinstance(item.get("link", ""), str) or not isinstance(item.get("disabled", False), bool):
193
+ raise ValueError("item link/disabled")
175
194
  for key in ("old", "new"):
176
195
  if item[key] is not None:
177
196
  validate_entries(item[key])
@@ -200,11 +219,42 @@ class Store:
200
219
  finally:
201
220
  os.close(fd)
202
221
 
203
- @staticmethod
204
- def work(target, txid):
222
+ def work(self, target, txid):
223
+ from .guide import shielded
224
+ target = checked(target)
225
+ key = hashlib.sha256(str(target).encode()).hexdigest()[:20]
226
+ # Beside the discovery dir, unless that parent is protected (the Uniflow
227
+ # config): then staging/backups stay in the private state root instead.
228
+ base = self.root / "work" if shielded(target.parent / ".crow-skills", self.home) else target.parent / ".crow-skills"
229
+ return checked(base / key / txid)
230
+
231
+ def existing_work(self, target, txid):
232
+ """Backups of a transaction, wherever the version that made it put them: older
233
+ transactions kept them beside the target even where that is now protected."""
234
+ primary = self.work(target, txid)
205
235
  target = checked(target)
206
236
  key = hashlib.sha256(str(target).encode()).hexdigest()[:20]
207
- return checked(target.parent / ".crow-skills" / key / txid)
237
+ for candidate in (primary, target.parent / ".crow-skills" / key / txid, self.root / "work" / key / txid):
238
+ if os.path.lexists(candidate):
239
+ return checked(candidate)
240
+ return primary
241
+
242
+ def managed_target(self, resolved, name):
243
+ """resolved is a package this CLI manages somewhere and its files match the manifest."""
244
+ directory = self.root / "targets"
245
+ if not directory.is_dir():
246
+ return False
247
+ for state_file in directory.glob("*/state.json"):
248
+ try:
249
+ data = read_json(state_file)
250
+ validate_state(data, data["target"])
251
+ record = data["installed"].get(name)
252
+ place = Path(data["target"]) / name
253
+ if record and os.path.realpath(place) == resolved and snapshot(place) == record["entries"]:
254
+ return True
255
+ except (CrowError, KeyError, TypeError, OSError):
256
+ continue
257
+ return False
208
258
 
209
259
  def plan(self, command, names, targets, source=None, replace=False, overwrite=False):
210
260
  if self.pending():
@@ -220,16 +270,45 @@ class Store:
220
270
  raise CrowError("unsafe_destination", "Não instale sobre o próprio repositório fonte.")
221
271
  state = self.state(target)
222
272
  key = str(target)
273
+ from .guide import protect_skill
274
+ for name in (names or state["installed"]):
275
+ try:
276
+ protect_skill(target / valid_name(name), self.home)
277
+ except CrowError as exc:
278
+ if exc.code != "protected_path" or name not in state["installed"]:
279
+ raise
280
+ if command != "uninstall":
281
+ raise CrowError("protected_path", str(exc) + " Instalação anterior a esta proteção: remova com "
282
+ "crow-skills uninstall " + name + " --dest " + str(target) + " --yes e, se for para o "
283
+ "Uniflow, reinstale com --agents uniflow --global.") from exc
284
+ # Only the uniflow agent replaces Uniflow's import links; --dest keeps refusing links.
285
+ store_target = "uniflow" in target_info["agents"] and target == uniflow_store(self.home)
223
286
  before[key] = state
224
287
  after[key] = json.loads(json.dumps(state))
225
288
  selected = list(names) if names else list(state["installed"])
226
289
  for name in selected:
227
290
  valid_name(name)
228
291
  record = state["installed"].get(name)
229
- current = snapshot(target / name)
292
+ dest = target / name
293
+ link, managed_link = None, False
294
+ if store_target and dest.is_symlink():
295
+ # Uniflow imports a native copy as a link store/crow-x -> its realpath.
296
+ link = os.readlink(dest)
297
+ resolved = os.path.realpath(dest)
298
+ managed_link = self.managed_target(resolved, name)
299
+ if not (managed_link or replace or overwrite):
300
+ raise CrowError("unmanaged_link", str(dest) + " é um link (do Uniflow) para " + resolved +
301
+ ", que não é uma instalação gerenciada pelo crow-skills. Para trocar só o link por uma "
302
+ "cópia gerenciada, sem tocar no alvo: crow-skills install " + name +
303
+ " --agents uniflow --global --replace --yes. Ou remova o link você mesmo.")
304
+ current = None
305
+ else:
306
+ current = snapshot(dest)
307
+ disabled = bool(record and current is not None and current == disabled_variant(record["entries"]))
308
+ effective = record["entries"] if disabled else current
230
309
  if command != "install" and record is None:
231
310
  raise CrowError("not_managed", "Skill não gerenciada neste destino: " + name)
232
- if record and current != record["entries"] and not overwrite:
311
+ if record and effective != record["entries"] and not overwrite and not managed_link:
233
312
  raise CrowError("local_changes", "Skill modificada/ausente: " + name + ". Revise; --overwrite-modified autoriza preservar backup e substituir/remover.")
234
313
  if not record and current is not None and not replace:
235
314
  raise CrowError("unmanaged_conflict", "Destino já existe e não é gerenciado: " + name + ". Use --replace após revisar.")
@@ -240,12 +319,19 @@ class Store:
240
319
  if name not in packages:
241
320
  packages[name] = source.package(name)
242
321
  new = packages[name].entries
243
- if (record and command != "uninstall" and current == new
322
+ if (record and command != "uninstall" and link is None and effective == new
244
323
  and record["revision"] == source.revision and record.get("source") == str(source.path)):
245
324
  output.append({"name": name, "target": key, "agents": target_info["agents"], "action": "unchanged"})
246
325
  continue
247
326
  action = "remove" if command == "uninstall" else ("replace" if current is not None else "install")
248
- items.append({"name": name, "target": key, "old": current, "new": new})
327
+ if link is not None:
328
+ action = "remove_link" if command == "uninstall" else "replace_link"
329
+ item = {"name": name, "target": key, "old": current, "new": disabled_variant(new) if disabled and new else new}
330
+ if link is not None:
331
+ item["link"] = link
332
+ if disabled and new:
333
+ item["disabled"] = True # Uniflow disabled it; the update keeps it disabled.
334
+ items.append(item)
249
335
  output.append({"name": name, "target": key, "agents": target_info["agents"], "action": action})
250
336
  if command == "uninstall":
251
337
  after[key]["installed"].pop(name)
@@ -267,7 +353,7 @@ class Store:
267
353
  if self.state(target) != state:
268
354
  raise CrowError("changed_during_plan", "O estado mudou após o preview; execute novamente.")
269
355
  for item in plan["items"]:
270
- if snapshot(Path(item["target"]) / item["name"]) != item["old"]:
356
+ if not self.matches(Path(item["target"]) / item["name"], item):
271
357
  raise CrowError("changed_during_plan", "Um destino mudou após o preview; execute novamente.")
272
358
  txid = uuid.uuid4().hex
273
359
  data = {k: plan[k] for k in ("before", "after", "items")}
@@ -285,6 +371,8 @@ class Store:
285
371
  if item["new"] is not None:
286
372
  new = self.work(item["target"], txid) / "new" / item["name"]
287
373
  plan["packages"][item["name"]].materialize(new)
374
+ if item.get("disabled"):
375
+ os.replace(new / "SKILL.md", new / DISABLED)
288
376
  if snapshot(new) != item["new"]:
289
377
  raise CrowError("staging_failed", "Cópia staged não corresponde ao pacote validado.")
290
378
  atomic_json(journal, data)
@@ -293,9 +381,9 @@ class Store:
293
381
  ensure_dir(target)
294
382
  dest = target / item["name"]
295
383
  work = self.work(target, txid)
296
- if snapshot(dest) != item["old"]:
384
+ if not self.matches(dest, item):
297
385
  raise CrowError("changed_during_install", "Destino alterado durante instalação.")
298
- if item["old"] is not None:
386
+ if item["old"] is not None or item.get("link") is not None: # a link moves as itself, never its target
299
387
  os.replace(dest, work / "old" / item["name"])
300
388
  if item["new"] is not None:
301
389
  os.replace(work / "new" / item["name"], dest)
@@ -315,6 +403,12 @@ class Store:
315
403
  raise
316
404
  return txid
317
405
 
406
+ @staticmethod
407
+ def matches(dest, item):
408
+ if item.get("link") is not None:
409
+ return same_link(dest, item["link"])
410
+ return snapshot(dest) == item["old"]
411
+
318
412
  def _restore(self, data):
319
413
  """Validate every target/backup before changing any during recovery."""
320
414
  operations = []
@@ -323,8 +417,18 @@ class Store:
323
417
  if current != before and current != data["after"][target]:
324
418
  raise CrowError("recovery_conflict", "Manifesto mudou após a transação; recuperação recusada.")
325
419
  for item in reversed(data["items"]):
326
- dest = checked(Path(item["target"]) / item["name"])
327
- old = self.work(item["target"], data["id"]) / "old" / item["name"]
420
+ dest = checked(Path(item["target"])) / item["name"]
421
+ old = self.existing_work(item["target"], data["id"]) / "old" / item["name"]
422
+ if item.get("link") is not None:
423
+ if same_link(old, item["link"]):
424
+ current = None if not os.path.lexists(dest) else ("link" if dest.is_symlink() else snapshot(dest))
425
+ if current not in (None, item["new"]):
426
+ raise CrowError("recovery_conflict", "Destino do link substituído foi alterado; recuperação recusada.")
427
+ operations.append((dest, old))
428
+ elif not same_link(dest, item["link"]):
429
+ raise CrowError("recovery_conflict", "Link original ausente; recuperação recusada.")
430
+ continue
431
+ dest = checked(dest)
328
432
  backup = snapshot(old)
329
433
  current = snapshot(dest)
330
434
  if backup is not None:
@@ -342,7 +446,9 @@ class Store:
342
446
  data["phase"] = "restoring"
343
447
  atomic_json(self.root / "transactions" / (data["id"] + ".json"), data)
344
448
  for dest, backup in operations:
345
- if dest.exists():
449
+ if dest.is_symlink():
450
+ dest.unlink()
451
+ elif dest.exists():
346
452
  shutil.rmtree(dest)
347
453
  if backup is not None:
348
454
  os.replace(backup, dest)
@@ -390,10 +496,30 @@ class Store:
390
496
  "note": "Nome externo ao catálogo: não gerenciado nem removido por este CLI."})
391
497
  continue
392
498
  record = state["installed"].get(name)
499
+ link = target / name
500
+ store = uniflow_store(self.home)
501
+ if link.is_symlink() and target == store:
502
+ resolved = os.path.realpath(link)
503
+ managed = self.managed_target(resolved, name)
504
+ rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
505
+ "status": "external_link", "revision": None, "activation_verified": False,
506
+ "note": "Link no store do Uniflow para " + resolved +
507
+ (" (cópia gerenciada pelo crow-skills)" if managed else " (não gerenciado)") +
508
+ ". Troque só o link por uma cópia gerenciada: crow-skills install " + name +
509
+ " --agents uniflow --global" + ("" if managed else " --replace") + " --yes"})
510
+ continue
511
+ if (record is None and link.is_symlink() and not (store / name).is_symlink() and (store / name).is_dir()
512
+ and os.path.realpath(target) != os.path.realpath(store)
513
+ and os.path.realpath(link) == os.path.realpath(store / name)):
514
+ rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
515
+ "status": "uniflow_link", "revision": None, "activation_verified": False,
516
+ "note": "Link projetado pelo Uniflow a partir do seu store; gerenciado em --agents uniflow."})
517
+ continue
393
518
  try:
394
519
  entries = snapshot(target / name)
395
520
  status = ("absent" if entries is None else "unmanaged") if record is None else (
396
- "missing" if entries is None else "installed" if entries == record["entries"] else "modified")
521
+ "missing" if entries is None else "installed" if entries == record["entries"] else
522
+ "disabled" if entries == disabled_variant(record["entries"]) else "modified")
397
523
  except CrowError:
398
524
  status = "unsafe"
399
525
  rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
package/docs/cli.md CHANGED
@@ -1,4 +1,4 @@
1
- # crow-skills CLI 0.6.0
1
+ # crow-skills CLI 0.7.0
2
2
 
3
3
  CLI local-first para humanos e agentes. Python 3.10+, sem bibliotecas Python externas; Git é necessário para fontes em clone, não para o snapshot npm. O launcher npm requer Node 18+. Mutações suportadas em macOS/Linux (locks POSIX). Não instala o agente, não executa scripts das skills e não muda modelos, prompts base ou permissões. Instruções de uso das skills (o guia do agente, abaixo) são gravadas pelo comando `guide` e, por padrão, por `install`/`update`/`uninstall` em `--global` quando a crow-behaviour fica instalada para um agente com mecanismo verificado; `--no-guide` desliga isso. Os mesmos comandos também aplicam as [integrações de interface](#integrações-de-interface-integrations) (status line do Claude Code e do Codex, segmento tmux); `--no-integrations` desliga. `crow-skills progress` mostra o [painel de progresso](#progresso-progress) da execução crow-behaviour ativa.
4
4
 
@@ -31,7 +31,7 @@ Para testar/distribuir o arquivo local sem registry:
31
31
 
32
32
  ```sh
33
33
  npm pack
34
- npm install -g ./andespindola-crow-skills-0.6.0.tgz --ignore-scripts
34
+ npm install -g ./andespindola-crow-skills-0.7.0.tgz --ignore-scripts
35
35
  ```
36
36
 
37
37
  O pacote contém o CLI Python e todas as skills do commit, com `release.json` de hashes. Não requer acesso ao GitHub ou clone para list/install. O launcher Node encaminha argumentos sem shell para Python em modo isolado, evitando imports pelo diretório do chamador/PYTHONPATH. `CROW_SKILLS_PYTHON` pode apontar para outro executável Python 3.10+. Não há dependências npm nem hooks de instalação que instalem skills automaticamente.
@@ -97,7 +97,7 @@ crow-skills recover --yes
97
97
  ```
98
98
 
99
99
  - Manifestos/locks/journals ficam sob `~/.local/state/crow-skills/`, separados da descoberta de skills. Não se escreve `manifest.json` dentro de `.claude/skills`.
100
- - Staging e backups ficam no diretório reservado `.crow-skills/` ao lado da raiz de skills, na mesma filesystem para permitir renames, nunca dentro dela. Backups são privados e retidos; não há expiração automática nesta versão.
100
+ - Staging e backups ficam no diretório reservado `.crow-skills/` ao lado da raiz de skills, na mesma filesystem para permitir renames, nunca dentro dela. Quando esse lado é protegido (o store do Uniflow), ficam em `~/.local/state/crow-skills/work/`. Backups são privados e retidos; não há expiração automática nesta versão.
101
101
  - Todos os destinos passam por preflight antes de aplicar. Renames são atômicos por pasta; uma operação com múltiplos destinos não é um único rename atômico global. Erros comuns provocam restauração; interrupções deixam journal recuperável, bloqueando novas mutações até `recover`.
102
102
  - `recover` restaura o estado anterior, não tenta terminar silenciosamente a instalação. A própria restauração tem um estado pendente `restoring`, permitindo retomada se também for interrompida. Confere arquivos/backup antes de substituir. Se houver edições após a interrupção, recusa e conserva os dados para resolução manual.
103
103
  - `install`/`update`/`uninstall` que também alteram o guia gravam duas transações encadeadas: skills e depois guia, com o journal do guia apontando a de skills (`linked`). As duas IDs saem no resultado (`transaction` e `guides.transaction`). Se o guia falhar, as skills dessa execução são desfeitas antes do erro. `rollback` da transação de skills desfaz primeiro o guia ligado; `rollback` da transação de guia desfaz só o guia. `recover` de um guia interrompido também desfaz as skills ligadas (`then_rollback` no preview). Uma interrupção exatamente entre as duas deixa as skills aplicadas e o guia `outdated`/`absent`, sem journal pendente: repita o comando ou faça rollback da transação de skills.
@@ -135,11 +135,35 @@ Erros retornam `ok:false` e `error.code/message`. Saídas reais incluem alteraç
135
135
  | gemini | `~/.gemini/skills` | `.gemini/skills` |
136
136
  | devin | `~/.config/devin/skills` (ou XDG_CONFIG_HOME) | `.devin/skills` |
137
137
  | shared | `~/.agents/skills` | `.agents/skills` |
138
+ | uniflow | store do Uniflow: `~/.config/uniflow/skills` (veja [Uniflow](#uniflow)) | não existe; recusado com `unsupported_scope` |
138
139
 
139
140
  Codex/shared são deduplicados quando apontam para o mesmo diretório. O diretório compartilhado também pode ser lido por outros hosts; não é isolamento entre agentes. `.codex/skills` é uma localização legada encontrada em ambientes existentes; o adaptador usa a localização moderna documentada. Para um destino legado específico, use `--dest` explicitamente.
140
141
 
141
142
  `agents` só verifica PATH e caminhos, sem iniciar agente/LLM. `status`/`doctor` conferem arquivos e estado de instalação, não provam carregamento, ativação, independência dos workers ou execução correta da skill. Gemini oferece `/skills reload`; para outros hosts, confira sua lista/reload ou reinicie a sessão conforme a versão. Nenhum hook ou system prompt base é alterado para forçar uso; o guia abaixo, aplicado por padrão em `--global` e desligável com `--no-guide`, é aditivo.
142
143
 
144
+ ### Uniflow
145
+
146
+ ```sh
147
+ crow-skills install --all --agents uniflow --global --dry-run
148
+ crow-skills install --all --agents uniflow --global --yes
149
+ crow-skills status --agents uniflow
150
+ crow-skills update --agents uniflow --global --yes
151
+ crow-skills uninstall --all --agents uniflow --global --yes
152
+ ```
153
+
154
+ O Uniflow guarda as skills num store próprio e cria, ao subir cada CLI, links desse store em `~/.claude/skills`, `~/.codex/skills`, `~/.gemini/skills` e `~/.config/devin/skills`; quando o CLI já tem uma entrada nativa com o mesmo nome, ele pula o link. O agente `uniflow` grava nesse store uma cópia real e gerenciada de cada pacote, com o mesmo manifesto e hashes dos outros agentes; `status`, `update`, `uninstall`, `--overwrite-modified`, `rollback` e `recover` funcionam igual.
155
+
156
+ - Store: o diretório `skills` ao lado do `config.json` do Uniflow, resolvido como ele resolve: `UNIFLOW_CONFIG` (ou `CROWFLOW_CONFIG`), senão `$XDG_CONFIG_HOME/uniflow`, senão `~/.config/uniflow`. Com `--home` isolado, essas variáveis são ignoradas.
157
+ - Proteção: em todo o diretório de configuração do Uniflow, só `skills/crow-*` é gravável. `config.json`, prompts, backups e as outras skills do store continuam recusados com `protected_path`, inclusive por `--dest`. A comparação usa também a identidade do arquivo, então outra grafia (`~/.config/UNIFLOW` no APFS) não fura a proteção. Se `UNIFLOW_CONFIG` aponta para um arquivo direto no home (ou num ancestral dele), só esse arquivo, o store e os marcadores do Uniflow (`.bundled-skills.json`, `skills.required.json`) são protegidos, nunca o home. Staging e backups dessa instalação ficam em `~/.local/state/crow-skills/work/`, não ao lado do store; `rollback`/`recover` de transações antigas procuram os backups nos dois lugares.
158
+ - Instalações antigas com `--dest` dentro da configuração do Uniflow (fora de `skills/crow-*`): `uninstall` continua permitido para o que está no manifesto; `install`/`update` são recusados com o comando de remoção e a sugestão de reinstalar com `--agents uniflow --global`.
159
+ - Links importados pelo Uniflow: na migração, o Uniflow pode criar `skills/crow-x` como link para a cópia nativa (ex.: `~/.claude/skills/crow-x`). `status`/`doctor` mostram `external_link` (doctor `healthy:false`) com o comando para resolver. `install`/`update` com `--agents uniflow` trocam só o link por uma cópia gerenciada (ação `replace_link`), sem tocar no alvo, e o `rollback` devolve o link. Isso é automático quando o alvo é uma instalação gerenciada pelo crow-skills com arquivos iguais ao manifesto; para outro alvo, o erro `unmanaged_link` explica e pede `--replace`. Links continuam recusados no resto do caminho (store como link, `--dest`).
160
+ - Skills desabilitadas no Uniflow (`SKILL.md` renomeado para `SKILL.md.disabled`): `status` mostra `disabled`, o doctor não acusa problema e `update` (mesmo com `--overwrite-modified`) mantém a skill desabilitada.
161
+ - Só `--global`: o Uniflow não tem store de projeto (`--project` falha com `unsupported_scope`).
162
+ - Sem guia: o Uniflow infere o uso da crow-behaviour por conta própria, então `guide` recusa `uniflow` (`unsupported_guide`) e `install` informa isso em `guides.skipped`. Integrações de interface também não se aplicam.
163
+ - Detecção: `agents` marca `detected` quando `uniflow` está no PATH (`executable_found`) ou o diretório de configuração existe (`config_found`).
164
+ - Cópias nativas duplicadas: se claude, gemini ou devin já têm um pacote crow de mesmo nome, o Uniflow não cria o link para ele e a cópia nativa continua valendo ali, possivelmente em outra revisão (`effect: skipped`). No codex é pior: o crow-skills instala em `~/.agents/skills`, o Uniflow liga o store em `~/.codex/skills` e o Codex lê os dois, vendo duas skills de mesmo nome (`effect: both`). `install`/`update` com `uniflow` listam essas cópias em `uniflow.native_duplicates` (diretórios que não puderam ser lidos, como um `~/.claude` symlink de dotfiles, vão para `uniflow.uninspected` sem derrubar o comando) e sugerem em `uniflow.suggested_command` o `uninstall` explícito; nada é removido sem esse comando. Remover a cópia nativa da crow-behaviour remove também o guia daquele agente, que deixa de valer fora do Uniflow.
165
+ - Links projetados: em `status`/`doctor`, um link do Uniflow para o store num diretório de agente aparece como `uniflow_link`, e não deixa o doctor `healthy:false`.
166
+
143
167
  Fontes primárias consultadas em 2026-10-20: [Codex](https://learn.chatgpt.com/docs/build-skills#where-codex-loads-local-skills), [Claude](https://code.claude.com/docs/en/skills), [Gemini](https://geminicli.com/docs/cli/using-agent-skills/), [Devin](https://docs.devin.ai/cli/extensibility/skills/overview).
144
168
 
145
169
  ## Instruções para o agente (guide)
@@ -159,7 +183,7 @@ Aplicação automática (padrão em `install`, `update` e `uninstall` com `--glo
159
183
 
160
184
  - O guia é planejado junto com as skills, a partir do conjunto de skills que o agente terá depois do comando, e conferido antes de gravar qualquer coisa. Install/update que deixam a crow-behaviour instalada aplicam o guia ou o regeneram; mudança de outra skill gerenciada regenera um guia já instalado (também quando a mudança chega pelo diretório compartilhado `~/.agents/skills`, que é o do codex). Uninstall da crow-behaviour remove o guia, devolvendo o arquivo que `--replace` havia deslocado; uninstall de outra skill regenera o guia instalado e nunca cria um novo.
161
185
  - Recusa do guia (arquivo não gerenciado sem `--replace`, guia editado/ausente sem `--overwrite-modified`, chave `developer_instructions` alheia, symlink, hard link, prompt compartilhado ou Uniflow) falha o comando inteiro, com código do guia e sugestão de `--replace`, `--overwrite-modified` ou `--no-guide`, sem gravar skills nem guia. Em install/update, `--replace` e `--overwrite-modified` valem para skills e guia.
162
- - `shared`, `--project` e `--dest` não têm mecanismo: o comando segue e informa em `guides.skipped`. Sem Python 3.11+ (tomllib), o guia do codex também é pulado e informado.
186
+ - `shared`, `uniflow`, `--project` e `--dest` não têm mecanismo: o comando segue e informa em `guides.skipped`. Sem Python 3.11+ (tomllib), o guia do codex também é pulado e informado.
163
187
  - `--dry-run` inclui `guides.changes` e `guides.preview` (conteúdo que seria gravado). A saída JSON traz `guides` com `changes`, `skipped`, `revision` e `transaction`.
164
188
 
165
189
  | Agente | Mecanismo (usuário) | Propriedade |
@@ -171,8 +195,8 @@ Aplicação automática (padrão em `install`, `update` e `uninstall` com `--glo
171
195
 
172
196
  Cada mecanismo foi verificado em 2026-10-21 (Claude Code 2.1.304, Codex CLI 0.163.0, agy 1.4.3, Devin CLI 3000.11.92) com uma sentinela carregada numa sessão nova junto do prompt base já existente: os mecanismos são aditivos. Versões futuras dos CLIs podem mudar isso; confira abrindo uma sessão nova. O comando não inicia agentes e retorna `activation_verified:false`.
173
197
 
174
- - Só `--global` é suportado. No comando `guide`, `--project`, `--dest` e `shared` são recusados com `unsupported_guide`, porque não há mecanismo verificado.
175
- - Nunca grava em symlinks, em arquivos com hard links, em prompts compartilhados (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`, `~/.config/devin/CLAUDE.md`), em `~/.config/uniflow/` ou nos alvos desses links. Não usa mecanismos que substituem o prompt base (como `model_instructions_file` ou `AGENTS.override.md`).
198
+ - Só `--global` é suportado. No comando `guide`, `--project`, `--dest`, `shared` e `uniflow` são recusados com `unsupported_guide`, porque não há mecanismo verificado.
199
+ - Nunca grava em symlinks, em arquivos com hard links, em prompts compartilhados (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`, `~/.config/devin/CLAUDE.md`), em `~/.config/uniflow/` (só `skills/crow-*` é gravável, e só pela instalação de skills) ou nos alvos desses links. Não usa mecanismos que substituem o prompt base (como `model_instructions_file` ou `AGENTS.override.md`).
176
200
  - No Codex, uma chave `developer_instructions` definida fora do bloco é recusada sempre, mesmo com `--replace`. A edição é validada por parse TOML (Python 3.11+): o restante do arquivo precisa continuar idêntico. Uninstall devolve o arquivo byte a byte.
177
201
  - Arquivo existente não gerenciado exige `--replace`; guia editado ou ausente exige `--overwrite-modified`. Ambos guardam backup privado em `~/.local/state/crow-skills/guide-backups/`. O uninstall devolve o arquivo que `--replace` substituiu (conferido pelo SHA-256 do backup), em vez de apagá-lo, e mantém um `config.toml` que já existia, mesmo vazio.
178
202
  - Transações usam o mesmo journal, lock, `rollback` e `recover` das skills. Se o Codex reescrever outras partes do `config.toml` depois, o rollback desfaz só o bloco.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/crow-skills",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Versioned, complementary agent skills with a protected multi-agent installer",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
package/release.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "README.md": {
4
4
  "executable": false,
5
5
  "kind": "file",
6
- "sha256": "e9ee4a4dc7d353c6b8a95bca69496fa3438e69d2f5346ea9165300b9b59622cb"
6
+ "sha256": "c5093abfec8d8ee55d73e81fa2130f02c93a1fb53c998fc3dd9bafc4d989bd16"
7
7
  },
8
8
  "bin/npm-crow-skills.cjs": {
9
9
  "executable": true,
@@ -18,7 +18,7 @@
18
18
  "crow_skills/__init__.py": {
19
19
  "executable": false,
20
20
  "kind": "file",
21
- "sha256": "0f74b89edf5c48e303404495e3884a27f854a7d4320501af510af8722564153d"
21
+ "sha256": "b7e23c635664d60f1afc60f98f3e68d7558fe4357973bb5b0d4592e7822a69aa"
22
22
  },
23
23
  "crow_skills/__main__.py": {
24
24
  "executable": false,
@@ -28,17 +28,17 @@
28
28
  "crow_skills/agents.py": {
29
29
  "executable": false,
30
30
  "kind": "file",
31
- "sha256": "36599a33cc707408216ee13880bd8e12c73b42803ccd160327c05042e464fec0"
31
+ "sha256": "b14c37d2f1becfe38c74411fc413608b375c05bd6961a0757e3ec085ec2e1f66"
32
32
  },
33
33
  "crow_skills/cli.py": {
34
34
  "executable": false,
35
35
  "kind": "file",
36
- "sha256": "215d90d0d503d98f2262c1b5c7612546309d034959f892f87118dabd4e9bae7c"
36
+ "sha256": "fd3c4e63be95f0a616932438f1d2648facffabf825a89bb15d9f1a466d33b075"
37
37
  },
38
38
  "crow_skills/guide.py": {
39
39
  "executable": false,
40
40
  "kind": "file",
41
- "sha256": "65b93fff61c496f7d3e1b3b32eadb008ba3a9cfd543a4db53f73becdb4ceec6f"
41
+ "sha256": "cabfe0809c0fc607b7deac0cd8df9025acaadddf4deb05f45288158dc51b0391"
42
42
  },
43
43
  "crow_skills/integrations.py": {
44
44
  "executable": false,
@@ -63,7 +63,7 @@
63
63
  "crow_skills/storage.py": {
64
64
  "executable": false,
65
65
  "kind": "file",
66
- "sha256": "2c3823f783c1da2f971860e6207a5146275e105fab74a7aa54e57033cd3993f0"
66
+ "sha256": "bf3c4bcb83888b74c72773aa1d0c0def4b3ba08f0c7bdbb918d2c549438e4d83"
67
67
  },
68
68
  "docs/cli-security.md": {
69
69
  "executable": false,
@@ -73,12 +73,12 @@
73
73
  "docs/cli.md": {
74
74
  "executable": false,
75
75
  "kind": "file",
76
- "sha256": "526b15dbd84bc8a04840f378128bcd20a5f17f99700b95aae42857c5732f7f18"
76
+ "sha256": "dd869d0e4e3354954436e78d0bde0254ee86da752195a7f55d9e7a8e5a628bdb"
77
77
  },
78
78
  "package.json": {
79
79
  "executable": false,
80
80
  "kind": "file",
81
- "sha256": "87790728d4db8dbb0a33888e315735e313a53acbebe56017012ad1dff5f62e6c"
81
+ "sha256": "ae9338cf496e3fb2691abed08474ad6a85c85932d5bf66c9a5c3a6b106208d2e"
82
82
  },
83
83
  "skills/crow-accessibility/SKILL.md": {
84
84
  "executable": false,
@@ -108,7 +108,7 @@
108
108
  "skills/crow-behaviour/SKILL.md": {
109
109
  "executable": false,
110
110
  "kind": "file",
111
- "sha256": "d2ae654e25b61603d54e67b6bf139333acd73638c950bf3da5acbd640d723eaa"
111
+ "sha256": "0e1360266fb7b5943f4fc0f9710b2294db08e14400a742d441deb23567a0ea2b"
112
112
  },
113
113
  "skills/crow-behaviour/agents/openai.yaml": {
114
114
  "executable": false,
@@ -128,7 +128,7 @@
128
128
  "skills/crow-behaviour/references/progress-ledger.md": {
129
129
  "executable": false,
130
130
  "kind": "file",
131
- "sha256": "f2363856574e789a419d233dcf6811216038e90ac52380734ca71008f8ff5724"
131
+ "sha256": "70a03809def8c65fa1c6390d57158cfdcd553246c4a91382feb35927a84b3af9"
132
132
  },
133
133
  "skills/crow-behaviour/references/quality-patterns.md": {
134
134
  "executable": false,
@@ -631,7 +631,7 @@
631
631
  "sha256": "4b921ea30ae9b731b5210581873e3d180e830cadfdc02bdbb5b3419ec39baaab"
632
632
  }
633
633
  },
634
- "revision": "6e784e58099c69ce065f1c19857782dc3e2a64c5",
634
+ "revision": "07e8fbd997cfbeb645fb0b986725934ebb243ce8",
635
635
  "schema_version": 1,
636
- "version": "0.6.0"
636
+ "version": "0.7.0"
637
637
  }
@@ -40,7 +40,7 @@ Leia [padrões e critérios de conclusão](references/quality-patterns.md) quand
40
40
  - Um responsável por arquivo ou recurso mutável em cada momento. Antes de escrita concorrente, use isolamento suportado e autorizado ou ownership disjunto. Não crie clones/worktrees por suposição. Sem isolamento seguro, serialize.
41
41
  - O principal pode avançar uma unidade independente enquanto aguarda, mas não refazer em paralelo a mesma unidade atribuída ao worker. Reatribua explicitamente se o executor falhar ou ficar indisponível.
42
42
  - Não deixe trabalhadores dispararem novas equipes sem escopo e limite atribuídos. Cancele trabalho obsoleto e aguarde processos relevantes antes de reiniciar ou declarar conclusão.
43
- - Mantenha o ledger de progresso: o coordenador é o único que grava; registre cada unidade ao iniciar e ao terminar, mostre o resumo compacto no chat a cada transição de fase e, se o host tiver uma ferramenta nativa de plano/tarefas, espelhe nela as mesmas unidades.
43
+ - Mantenha o ledger de progresso: o coordenador é o único que grava; registre cada unidade ao iniciar e ao terminar, mostre o progresso na própria conversa (plano no início, resumo compacto a cada transição de fase, linha curta quando algo muda) e, se o host tiver uma ferramenta nativa de plano/tarefas, espelhe nela as mesmas unidades. Nunca abra janela, painel ou outro processo para exibir progresso; veja [ledger de progresso](references/progress-ledger.md).
44
44
 
45
45
  ### 4. Verifique antes de sintetizar
46
46
 
@@ -72,7 +72,7 @@ Entregue o resultado pedido, apoiado pelo **execution-ledger**: cobertura de req
72
72
  - Relate o modo real quando relevante: workflow, subagentes ou sequencial. Não atribua trabalho a agentes que não existiram.
73
73
  - Não declare implementação testada porque um revisor aprovou, nem deploy porque testes passaram.
74
74
  - Registre o que ficou de fora e por quê; nenhum corte silencioso.
75
- - Ao terminar, feche o ledger (`done`, `failed` ou `cancelled`), feche o painel lateral que abriu e não deixe mutações/trabalhadores pendentes sem responsável informado.
75
+ - Ao terminar, feche o ledger (`done`, `failed` ou `cancelled`), marque as unidades no plano nativo do host e não deixe mutações/trabalhadores pendentes sem responsável informado.
76
76
 
77
77
  ## Limites e composição
78
78
 
@@ -66,17 +66,15 @@ Sem Python ou sem acesso ao script, escreva o JSON acima diretamente com as ferr
66
66
 
67
67
  ## O que mostrar ao usuário
68
68
 
69
- - A cada transição de fase e no fim, inclua no chat o resumo compacto (`crow_ledger.py show`): árvore de fases e unidades com barras ASCII. Não repita o resumo a cada unidade.
70
- - Se o host tiver ferramenta nativa de plano ou tarefas, espelhe nela as mesmas unidades e estados; algumas interfaces mostram esse plano na barra de status.
71
- - Tarefa grande (várias fases ou muitas unidades) rodando dentro do tmux (`$TMUX` definido): abra um painel lateral com o painel ao vivo e feche-o ao terminar. Use `crow-skills progress --watch` se o comando existir no PATH; ele mostra também tokens lidos dos logs do host e sai sozinho após o fim da execução.
69
+ O progresso aparece na própria conversa, exibido pelo agente que está trabalhando. Nunca abra janela, painel, aba, terminal, navegador ou outro processo para mostrar progresso (nada de `tmux split-window`, `new-window` ou `crow-skills progress --watch` em outro painel), nem peça ao usuário para abrir um.
72
70
 
73
- ```sh
74
- tmux split-window -h -d -l 64 -t "$TMUX_PANE" -c "$PWD" -P -F '#{pane_id}' "crow-skills progress --watch --run <run-id> --cwd '$PWD'"
75
- # ao terminar, se o painel ainda existir:
76
- tmux kill-pane -t <pane_id>
77
- ```
71
+ - **Início:** ao começar uma tarefa substantiva, mostre o plano em poucas linhas: fases, unidades e o que será entregue.
72
+ - **Plano nativo do host:** se o host tiver ferramenta de plano ou tarefas (lista de tarefas, plano de etapas), espelhe nela as mesmas unidades e mantenha os estados em dia: uma unidade em andamento por responsável, concluída assim que tiver evidência. É o jeito preferido de mostrar progresso contínuo, porque a interface do próprio host o exibe sem outra janela.
73
+ - **Transições:** a cada transição de fase e no fim, inclua no chat o resumo compacto (`crow_ledger.py show`): árvore de fases e unidades com barras ASCII. Não repita o resumo a cada unidade.
74
+ - **Entre transições:** uma linha curta quando uma unidade demorada começa ou termina, ou quando algo muda o plano (falha, achado, decisão pendente). Em espera longa (job em segundo plano, workflow, CI), diga o que está rodando e quando vai checar de novo.
75
+ - **Proporção:** o progresso informa, não substitui a resposta. Sem transcrição de coordenação, sem resumo repetido e sem barra inventada.
78
76
 
79
- Não abra painel fora do tmux, nem em tarefa pequena. Não instale nada para exibir progresso.
77
+ Não instale nada para exibir progresso.
80
78
 
81
79
  ## Limites
82
80