@topy-ai/maggie 0.7.45 → 0.7.47
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-zh-TW.md +15 -2
- package/README.md +55 -3
- package/bin/maggie.js +22 -4
- package/bundled-contracts/google-integrations/capability-report-v2.schema.json +76 -0
- package/bundled-contracts/google-integrations/external-write-readback-v1.schema.json +36 -0
- package/bundled-contracts/maggie-deployment/deployer-delegation-v1.schema.json +20 -0
- package/bundled-contracts/maggie-deployment/release-profile-v1.schema.json +34 -0
- package/bundled-contracts/maggie-design/browser-capability-v1.schema.json +33 -0
- package/bundled-contracts/maggie-feedback/evidence-bundle-v1.schema.json +38 -0
- package/bundled-references/browser-inspection.md +17 -0
- package/bundled-references/google-integrations-runbook.md +8 -1
- package/bundled-references/memory-hook.md +18 -5
- package/bundled-skills/maggie-clone/SKILL.md +7 -0
- package/bundled-skills/maggie-deployment/SKILL.md +55 -15
- package/bundled-skills/maggie-feedback/SKILL.md +26 -1
- package/bundled-skills/maggie-memory/SKILL.md +10 -1
- package/bundled-skills/maggie-ops/SKILL.md +22 -0
- package/bundled-skills/maggie-seo-geo/SKILL.md +4 -1
- package/bundled-tools/clis/maggie_analytics.py +8 -0
- package/bundled-tools/clis/maggie_browser_audit.py +13 -2
- package/bundled-tools/clis/maggie_deployment.py +214 -6
- package/bundled-tools/clis/maggie_feedback.py +116 -1
- package/bundled-tools/clis/maggie_memory.py +10 -3
- package/bundled-tools/clis/maggie_ops.py +33 -0
- package/bundled-tools/clis/maggie_release.py +124 -19
- package/bundled-tools/runtime/browser_capability.py +143 -0
- package/bundled-tools/runtime/external_write.py +122 -0
- package/bundled-tools/runtime/google_capabilities.py +37 -5
- package/bundled-tools/runtime/maggie_memory.py +17 -2
- package/package.json +1 -1
- package/references/browser-inspection.md +17 -0
- package/references/google-integrations-runbook.md +8 -1
- package/references/memory-hook.md +18 -5
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Validate bounded, hash-only evidence for an external provider write."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
SCHEMA = "maggie-external-write-readback.v1"
|
|
11
|
+
PROVIDERS = {"ga4", "gtm", "gsc", "google-ads", "firebase"}
|
|
12
|
+
STATUSES = {"created", "updated", "no-op"}
|
|
13
|
+
FINGERPRINT = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _safe_text(value: Any, field: str, errors: list[str], pattern: str | None = None) -> str | None:
|
|
17
|
+
if not isinstance(value, str) or not value.strip() or len(value) > 200 or "@" in value or re.search(r"(?:token|secret|password|BEGIN PRIVATE)", value, re.I):
|
|
18
|
+
errors.append(f"{field} must be a bounded redacted identifier")
|
|
19
|
+
return None
|
|
20
|
+
if pattern and not re.fullmatch(pattern, value):
|
|
21
|
+
errors.append(f"{field} has an invalid format")
|
|
22
|
+
return None
|
|
23
|
+
return value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate(value: Any) -> dict[str, Any]:
|
|
27
|
+
errors: list[str] = []
|
|
28
|
+
if not isinstance(value, dict):
|
|
29
|
+
return {"schemaVersion": "maggie-external-write-readback-result.v1", "passed": False, "errors": ["evidence must be an object"], "mutation": "not executed"}
|
|
30
|
+
if value.get("schemaVersion") != SCHEMA:
|
|
31
|
+
errors.append(f"schemaVersion must be {SCHEMA}")
|
|
32
|
+
if value.get("provider") not in PROVIDERS:
|
|
33
|
+
errors.append("provider is unsupported")
|
|
34
|
+
_safe_text(value.get("generatedAt"), "generatedAt", errors)
|
|
35
|
+
resource = _safe_text(value.get("resource"), "resource", errors)
|
|
36
|
+
_safe_text(value.get("operation"), "operation", errors, r"[a-z][a-z0-9_.-]{1,63}")
|
|
37
|
+
key = _safe_text(value.get("idempotencyKey"), "idempotencyKey", errors, r"[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}")
|
|
38
|
+
|
|
39
|
+
retry = value.get("retry")
|
|
40
|
+
if not isinstance(retry, dict) or set(retry) != {"attempt", "maxAttempts", "sameKeyOnRetry"}:
|
|
41
|
+
errors.append("retry must contain attempt, maxAttempts, and sameKeyOnRetry")
|
|
42
|
+
retry = {}
|
|
43
|
+
else:
|
|
44
|
+
attempt, maximum = retry.get("attempt"), retry.get("maxAttempts")
|
|
45
|
+
if not isinstance(attempt, int) or not 1 <= attempt <= 3:
|
|
46
|
+
errors.append("retry.attempt must be between 1 and 3")
|
|
47
|
+
if not isinstance(maximum, int) or not 1 <= maximum <= 3 or (isinstance(attempt, int) and isinstance(maximum, int) and attempt > maximum):
|
|
48
|
+
errors.append("retry.maxAttempts must be between attempt and 3")
|
|
49
|
+
if retry.get("sameKeyOnRetry") is not True:
|
|
50
|
+
errors.append("retry.sameKeyOnRetry must be true")
|
|
51
|
+
|
|
52
|
+
mutation = value.get("mutation")
|
|
53
|
+
if not isinstance(mutation, dict) or set(mutation) != {"status", "statusCode", "explicitConfirmation"}:
|
|
54
|
+
errors.append("mutation must contain status, statusCode, and explicitConfirmation")
|
|
55
|
+
mutation = {}
|
|
56
|
+
else:
|
|
57
|
+
if mutation.get("status") not in STATUSES:
|
|
58
|
+
errors.append("mutation.status is unsupported")
|
|
59
|
+
if not isinstance(mutation.get("statusCode"), int) or not 200 <= mutation["statusCode"] <= 299:
|
|
60
|
+
errors.append("mutation.statusCode must be a successful HTTP status")
|
|
61
|
+
if mutation.get("explicitConfirmation") is not True:
|
|
62
|
+
errors.append("mutation requires explicit confirmation")
|
|
63
|
+
if value.get("mutationExecuted") is not True:
|
|
64
|
+
errors.append("mutationExecuted must be true")
|
|
65
|
+
|
|
66
|
+
readback = value.get("readback")
|
|
67
|
+
safe_checks: list[dict[str, Any]] = []
|
|
68
|
+
if not isinstance(readback, dict) or set(readback) != {"passed", "endpoint", "checks"}:
|
|
69
|
+
errors.append("readback must contain passed, endpoint, and checks")
|
|
70
|
+
readback = {}
|
|
71
|
+
else:
|
|
72
|
+
if readback.get("passed") is not True:
|
|
73
|
+
errors.append("readback.passed must be true")
|
|
74
|
+
endpoint = readback.get("endpoint")
|
|
75
|
+
parsed = urlsplit(endpoint) if isinstance(endpoint, str) else None
|
|
76
|
+
if not parsed or parsed.scheme != "https" or not parsed.netloc or parsed.query or parsed.fragment or parsed.username or parsed.password:
|
|
77
|
+
errors.append("readback.endpoint must be an HTTPS URL without credentials/query")
|
|
78
|
+
checks = readback.get("checks")
|
|
79
|
+
if not isinstance(checks, list) or not checks:
|
|
80
|
+
errors.append("readback.checks must be a non-empty array")
|
|
81
|
+
checks = []
|
|
82
|
+
for index, check in enumerate(checks):
|
|
83
|
+
prefix = f"readback.checks[{index}]"
|
|
84
|
+
if not isinstance(check, dict) or set(check) != {"name", "expectedFingerprint", "observedFingerprint", "passed"}:
|
|
85
|
+
errors.append(f"{prefix} must contain name and hash-only invariant fields")
|
|
86
|
+
continue
|
|
87
|
+
name = _safe_text(check.get("name"), f"{prefix}.name", errors)
|
|
88
|
+
expected = check.get("expectedFingerprint")
|
|
89
|
+
observed = check.get("observedFingerprint")
|
|
90
|
+
if not isinstance(expected, str) or not FINGERPRINT.fullmatch(expected):
|
|
91
|
+
errors.append(f"{prefix}.expectedFingerprint must be sha256")
|
|
92
|
+
if not isinstance(observed, str) or not FINGERPRINT.fullmatch(observed):
|
|
93
|
+
errors.append(f"{prefix}.observedFingerprint must be sha256")
|
|
94
|
+
if check.get("passed") is not True or expected != observed:
|
|
95
|
+
errors.append(f"{prefix} does not match its expected readback")
|
|
96
|
+
safe_checks.append({"name": name, "passed": check.get("passed") is True})
|
|
97
|
+
|
|
98
|
+
duplicates = value.get("duplicates")
|
|
99
|
+
orphaned = value.get("orphaned")
|
|
100
|
+
if not isinstance(duplicates, list) or duplicates:
|
|
101
|
+
errors.append("duplicates must be an empty array after reconciliation")
|
|
102
|
+
if not isinstance(orphaned, list) or orphaned:
|
|
103
|
+
errors.append("orphaned must be an empty array after cleanup")
|
|
104
|
+
cleanup = value.get("cleanup")
|
|
105
|
+
if not isinstance(cleanup, dict) or set(cleanup) != {"status", "checked"} or cleanup.get("status") not in {"not-needed", "passed"} or cleanup.get("checked") is not True:
|
|
106
|
+
errors.append("cleanup must be checked and not-needed or passed")
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"schemaVersion": "maggie-external-write-readback-result.v1",
|
|
110
|
+
"passed": not errors,
|
|
111
|
+
"errors": sorted(set(errors)),
|
|
112
|
+
"provider": value.get("provider"),
|
|
113
|
+
"resource": resource,
|
|
114
|
+
"operation": value.get("operation"),
|
|
115
|
+
"idempotencyKey": key,
|
|
116
|
+
"retry": {"attempt": retry.get("attempt"), "maxAttempts": retry.get("maxAttempts"), "sameKeyOnRetry": retry.get("sameKeyOnRetry") is True},
|
|
117
|
+
"readback": {"passed": readback.get("passed") is True, "checks": safe_checks} if isinstance(readback, dict) else {"passed": False, "checks": []},
|
|
118
|
+
"duplicates": [],
|
|
119
|
+
"orphaned": [],
|
|
120
|
+
"cleanup": cleanup if isinstance(cleanup, dict) else {},
|
|
121
|
+
"mutation": "not executed",
|
|
122
|
+
}
|
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
|
7
7
|
from urllib.parse import urlsplit
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
SCHEMA_VERSION = "maggie-google-capability-report.
|
|
10
|
+
SCHEMA_VERSION = "maggie-google-capability-report.v2"
|
|
11
11
|
CAPABILITY_NAMES = ("read", "report", "edit", "publish")
|
|
12
12
|
CAPABILITY_STATES = {"verified", "not_tested", "not_available", "blocked"}
|
|
13
13
|
PROVIDERS = {"gsc", "ga4", "gtm", "google-ads", "firebase"}
|
|
@@ -112,6 +112,34 @@ def _write_test(value: Any, field: str, errors: list[str]) -> bool:
|
|
|
112
112
|
return valid and not unknown
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
def _verified_reference(value: Any, field: str, errors: list[str]) -> dict[str, Any]:
|
|
116
|
+
if not isinstance(value, dict) or set(value) != {"id", "verified"}:
|
|
117
|
+
errors.append(f"{field} must contain only id and verified")
|
|
118
|
+
return {"id": None, "verified": False}
|
|
119
|
+
identifier = value.get("id")
|
|
120
|
+
if not isinstance(identifier, str) or not identifier.strip() or len(identifier) > 200 or re.search(r"[@\s]", identifier):
|
|
121
|
+
errors.append(f"{field}.id must be a redacted non-email identifier")
|
|
122
|
+
if value.get("verified") is not True:
|
|
123
|
+
errors.append(f"{field}.verified must be true")
|
|
124
|
+
return {"id": identifier if isinstance(identifier, str) else None, "verified": value.get("verified") is True}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _target_reference(value: Any, resource: str, field: str, errors: list[str]) -> dict[str, Any]:
|
|
128
|
+
if not isinstance(value, dict) or set(value) != {"id", "kind", "verified"}:
|
|
129
|
+
errors.append(f"{field} must contain only id, kind, and verified")
|
|
130
|
+
return {"id": None, "kind": None, "verified": False}
|
|
131
|
+
identifier = value.get("id")
|
|
132
|
+
if not isinstance(identifier, str) or not identifier.strip() or len(identifier) > 200 or re.search(r"[@\s]", identifier):
|
|
133
|
+
errors.append(f"{field}.id must be a redacted non-email identifier")
|
|
134
|
+
if identifier != resource:
|
|
135
|
+
errors.append(f"{field}.id must equal the selected resource")
|
|
136
|
+
if value.get("kind") not in {"property", "container", "customer", "project", "site", "account"}:
|
|
137
|
+
errors.append(f"{field}.kind is unsupported")
|
|
138
|
+
if value.get("verified") is not True:
|
|
139
|
+
errors.append(f"{field}.verified must be true")
|
|
140
|
+
return {"id": identifier if isinstance(identifier, str) else None, "kind": value.get("kind"), "verified": value.get("verified") is True}
|
|
141
|
+
|
|
142
|
+
|
|
115
143
|
def _required_scope(provider: str, capability: str) -> str | None:
|
|
116
144
|
if capability == "read" or capability == "report":
|
|
117
145
|
return READ_SCOPES[provider]
|
|
@@ -124,9 +152,9 @@ def validate_report(report: Any) -> dict[str, Any]:
|
|
|
124
152
|
"""Return safe validation output; never returns arbitrary input fields."""
|
|
125
153
|
errors: list[str] = []
|
|
126
154
|
if not isinstance(report, dict):
|
|
127
|
-
return {"schemaVersion": "maggie-google-capability-report-result.
|
|
155
|
+
return {"schemaVersion": "maggie-google-capability-report-result.v2", "passed": False, "errors": ["report must be an object"], "providerResults": []}
|
|
128
156
|
if report.get("schemaVersion") != SCHEMA_VERSION:
|
|
129
|
-
errors.append("schemaVersion must be maggie-google-capability-report.v1")
|
|
157
|
+
errors.append("schemaVersion must be maggie-google-capability-report.v2; v1 reports lack target guard evidence")
|
|
130
158
|
if not _string(report.get("generatedAt"), "generatedAt", errors):
|
|
131
159
|
pass
|
|
132
160
|
if report.get("mutationsAllowed") is not False:
|
|
@@ -143,7 +171,7 @@ def validate_report(report: Any) -> dict[str, Any]:
|
|
|
143
171
|
if not isinstance(row, dict):
|
|
144
172
|
errors.append(f"{prefix} must be an object")
|
|
145
173
|
continue
|
|
146
|
-
required = {"provider", "resource", "authMode", "scopes", "productRole", "evidence", "capabilities", "nextAction"}
|
|
174
|
+
required = {"provider", "resource", "activeAccount", "target", "authMode", "scopes", "productRole", "evidence", "capabilities", "nextAction"}
|
|
147
175
|
unknown = sorted(set(row) - required)
|
|
148
176
|
if unknown:
|
|
149
177
|
errors.append(f"{prefix} contains unsupported fields")
|
|
@@ -154,6 +182,8 @@ def validate_report(report: Any) -> dict[str, Any]:
|
|
|
154
182
|
continue
|
|
155
183
|
if not _string(resource, f"{prefix}.resource", errors, max_length=200):
|
|
156
184
|
continue
|
|
185
|
+
active_account = _verified_reference(row.get("activeAccount"), f"{prefix}.activeAccount", errors)
|
|
186
|
+
target = _target_reference(row.get("target"), resource, f"{prefix}.target", errors)
|
|
157
187
|
key = (provider, resource)
|
|
158
188
|
if key in seen:
|
|
159
189
|
errors.append(f"{prefix} duplicates provider/resource")
|
|
@@ -226,6 +256,8 @@ def validate_report(report: Any) -> dict[str, Any]:
|
|
|
226
256
|
normalized.append({
|
|
227
257
|
"provider": provider,
|
|
228
258
|
"resource": resource,
|
|
259
|
+
"activeAccount": active_account,
|
|
260
|
+
"target": target,
|
|
229
261
|
"authMode": row.get("authMode"),
|
|
230
262
|
"scopes": sorted(scopes),
|
|
231
263
|
"productRole": row.get("productRole"),
|
|
@@ -241,7 +273,7 @@ def validate_report(report: Any) -> dict[str, Any]:
|
|
|
241
273
|
})
|
|
242
274
|
|
|
243
275
|
return {
|
|
244
|
-
"schemaVersion": "maggie-google-capability-report-result.
|
|
276
|
+
"schemaVersion": "maggie-google-capability-report-result.v2",
|
|
245
277
|
"passed": not errors,
|
|
246
278
|
"errors": sorted(set(errors)),
|
|
247
279
|
"providerResults": normalized,
|
|
@@ -73,7 +73,16 @@ def write(project: str | Path, kind: str, payload: dict[str, Any]) -> Path:
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
def init(project: str | Path) -> dict[str, str]:
|
|
76
|
-
|
|
76
|
+
paths: dict[str, str] = {}
|
|
77
|
+
for kind in KINDS:
|
|
78
|
+
path = memory_path(project, kind)
|
|
79
|
+
if path.exists():
|
|
80
|
+
# Validate existing state, but keep its bytes and mtime unchanged.
|
|
81
|
+
read(project, kind)
|
|
82
|
+
else:
|
|
83
|
+
write(project, kind, empty(kind))
|
|
84
|
+
paths[kind] = str(path)
|
|
85
|
+
return paths
|
|
77
86
|
|
|
78
87
|
|
|
79
88
|
def _text(value: Any, field: str, required: bool = False) -> str:
|
|
@@ -176,11 +185,13 @@ def _active(item: dict[str, Any], project_id: str = "") -> bool:
|
|
|
176
185
|
return not expires or expires > now()
|
|
177
186
|
|
|
178
187
|
|
|
179
|
-
def relevant(project: str | Path, *, skill: str = "", query: str = "", project_id: str = "", status: str | None = None) -> dict[str, list[dict[str, Any]]]:
|
|
188
|
+
def relevant(project: str | Path, *, skill: str = "", query: str = "", project_id: str = "", status: str | None = None, include_shared: bool = False) -> dict[str, list[dict[str, Any]]]:
|
|
180
189
|
terms = set(re.findall(r"[a-z0-9_-]+", f"{skill} {query}".lower()))
|
|
181
190
|
result: dict[str, list[dict[str, Any]]] = {kind: [] for kind in KINDS}
|
|
182
191
|
for kind in KINDS:
|
|
183
192
|
for item in read(project, kind)["items"]:
|
|
193
|
+
if not include_shared and item.get("scope") != "project":
|
|
194
|
+
continue
|
|
184
195
|
if status is not None:
|
|
185
196
|
if item.get("status") != status:
|
|
186
197
|
continue
|
|
@@ -194,6 +205,10 @@ def relevant(project: str | Path, *, skill: str = "", query: str = "", project_i
|
|
|
194
205
|
return result
|
|
195
206
|
|
|
196
207
|
|
|
208
|
+
def initialized(project: str | Path) -> bool:
|
|
209
|
+
return all(memory_path(project, kind).is_file() for kind in KINDS)
|
|
210
|
+
|
|
211
|
+
|
|
197
212
|
def transition(project: str | Path, kind: str, item_id: str, status: str) -> dict[str, Any]:
|
|
198
213
|
if status not in STATUSES:
|
|
199
214
|
raise ValueError(f"invalid status: {status}")
|
package/package.json
CHANGED
|
@@ -14,6 +14,23 @@ The browser capability must support:
|
|
|
14
14
|
- slow scrolling, click, hover, keyboard focus, and back/forward navigation;
|
|
15
15
|
- reading computed CSS, media sources, links, and visible accessibility labels.
|
|
16
16
|
|
|
17
|
+
Before navigation, Maggie runs a capability preflight against the configured
|
|
18
|
+
adapter. The report is `maggie-browser-capability.v1` and classifies the
|
|
19
|
+
adapter as `ready`, `missing`, or `incompatible`. This prevents a system
|
|
20
|
+
`browse`/`xdg-open` desktop opener from being mistaken for a browser adapter:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
maggie browser-audit https://example.com \
|
|
24
|
+
--browse "$HOME/.codex/skills/gstack/browse/dist/browse" \
|
|
25
|
+
--output .maggie/browser-audit --required body --check-browser
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The report contains only the adapter name, path fingerprints, capability
|
|
29
|
+
checks, and bounded installation/fallback guidance. If it is `missing` or
|
|
30
|
+
`incompatible`, install or enable an authorized Chrome/Playwright/gstack
|
|
31
|
+
adapter and pass its executable path. Maggie does not silently install a
|
|
32
|
+
browser runtime or use a desktop opener as a fallback.
|
|
33
|
+
|
|
17
34
|
The minimum extraction result for a target is:
|
|
18
35
|
|
|
19
36
|
```text
|
|
@@ -111,8 +111,13 @@ maggie ops google-capabilities \
|
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
The report contract is
|
|
114
|
-
[`capability-report-
|
|
114
|
+
[`capability-report-v2.schema.json`](../bundled-contracts/google-integrations/capability-report-v2.schema.json).
|
|
115
115
|
The CLI writes a normalized result and never copies arbitrary input fields.
|
|
116
|
+
Every provider row must also identify the redacted active account and selected
|
|
117
|
+
target resource, both with `verified: true`; the target ID must equal the
|
|
118
|
+
selected `resource`. A report that only proves account discovery, or comes from
|
|
119
|
+
the wrong browser account/property, is rejected before a write workflow can use
|
|
120
|
+
it.
|
|
116
121
|
|
|
117
122
|
## Capability matrix
|
|
118
123
|
|
|
@@ -121,6 +126,8 @@ Every provider/resource row must expose this shape:
|
|
|
121
126
|
| Field | Meaning |
|
|
122
127
|
|---|---|
|
|
123
128
|
| `provider` / `resource` | The Google product and scoped resource being checked |
|
|
129
|
+
| `activeAccount` | Redacted account/principal reference confirmed by the current auth session |
|
|
130
|
+
| `target` | Resource ID and kind confirmed in the current account; must match `resource` |
|
|
124
131
|
| `authMode` | `desktop-oauth`, `service-account-impersonation`, or `none` |
|
|
125
132
|
| `scopes` | Exact allowlisted OAuth scopes, never token values |
|
|
126
133
|
| `productRole` | Role granted in the product, distinct from Cloud IAM |
|
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
# Maggie Memory Hook
|
|
2
2
|
|
|
3
|
-
Every Maggie skill invocation
|
|
4
|
-
|
|
3
|
+
Every Maggie skill invocation uses the current project's Maggie Memory by
|
|
4
|
+
default. Project initialization creates `.maggie/memory/` for that repository.
|
|
5
5
|
|
|
6
6
|
## Before work
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Use the project-local adapter when it is present:
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
python3 tools/clis/maggie_memory.py context --project . --skill <current-skill>
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
If that adapter is absent, use the installed CLI:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
maggie memory context --project . --skill <current-skill>
|
|
18
|
+
```
|
|
19
|
+
|
|
14
20
|
Load the returned active preferences, conventions, lessons, and matching error
|
|
15
21
|
history into the current decision context. Surface material rules and verify
|
|
16
22
|
them against current project evidence. Memory never replaces inspection,
|
|
17
23
|
approval, bootstrap, or production gates.
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
The context response reports `status: "uninitialized"` when Maggie Memory is
|
|
26
|
+
available but the project has not initialized its store. In that case, explain
|
|
27
|
+
that `maggie init --scope project` initializes it; do not call memory
|
|
28
|
+
unavailable. Report unavailable only when neither the project adapter nor the
|
|
29
|
+
installed `maggie` CLI can run. Do not invent or silently reconstruct memory.
|
|
30
|
+
|
|
31
|
+
Context includes only active project-scope items for the current repository.
|
|
32
|
+
Include user/workspace entries only after a separate explicit opt-in with
|
|
33
|
+
`--include-shared`.
|
|
21
34
|
|
|
22
35
|
## After work
|
|
23
36
|
|