@blxzer/cursor-trellis 0.3.6 → 0.4.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 +23 -0
- package/README.md +6 -6
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +5 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +39 -2
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/constants/paths.d.ts +4 -0
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +4 -0
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.4.0.json +9 -0
- package/dist/templates/markdown/index.d.ts +6 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +6 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/guides/artifact-locale-guide.md.txt +93 -0
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +7 -7
- package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +10 -8
- package/dist/templates/markdown/spec/guides/debug-loop-guide.md.txt +227 -0
- package/dist/templates/markdown/spec/guides/goal-release-regression-runbook.md.txt +132 -0
- package/dist/templates/markdown/spec/guides/index.md.txt +37 -0
- package/dist/templates/markdown/spec/guides/prototype-guide.md.txt +139 -0
- package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +4 -0
- package/dist/templates/markdown/spec/guides/test-discipline-guide.md.txt +138 -0
- package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +1 -0
- package/dist/templates/trellis/index.d.ts +12 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +26 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/pool/README.md +103 -0
- package/dist/templates/trellis/pool/items/.gitkeep +0 -0
- package/dist/templates/trellis/pool/plan.md +26 -0
- package/dist/templates/trellis/scripts/common/pool_store.py +702 -0
- package/dist/templates/trellis/scripts/common/task_dashboard.py +8 -0
- package/dist/templates/trellis/scripts/common/task_dependencies.py +673 -0
- package/dist/templates/trellis/scripts/common/task_gates.py +58 -5
- package/dist/templates/trellis/scripts/common/task_store.py +256 -0
- package/dist/templates/trellis/scripts/common/test_depends_mode_block.py +489 -0
- package/dist/templates/trellis/scripts/common/test_pool_store.py +428 -0
- package/dist/templates/trellis/scripts/common/test_task_dependencies.py +345 -0
- package/dist/templates/trellis/scripts/pool.py +192 -0
- package/dist/templates/trellis/scripts/task.py +66 -1
- package/dist/templates/trellis/scripts/verify_evidence_probe.py +138 -0
- package/dist/templates/trellis/workflow.md +32 -2
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env python
|
|
2
2
|
"""
|
|
3
3
|
Task quality gate helpers.
|
|
4
4
|
|
|
@@ -15,6 +15,11 @@ from dataclasses import dataclass, field
|
|
|
15
15
|
from datetime import datetime, timezone
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
|
+
from .task_dependencies import (
|
|
19
|
+
describe_dependencies,
|
|
20
|
+
read_depends_mode,
|
|
21
|
+
scope_children_for_task,
|
|
22
|
+
)
|
|
18
23
|
from .task_map import (
|
|
19
24
|
get_child_state,
|
|
20
25
|
load_task_map,
|
|
@@ -144,6 +149,7 @@ STABLE_TASK_KEYS = (
|
|
|
144
149
|
"kind",
|
|
145
150
|
"mode",
|
|
146
151
|
"contract_epoch",
|
|
152
|
+
"depends_on",
|
|
147
153
|
)
|
|
148
154
|
|
|
149
155
|
|
|
@@ -161,6 +167,7 @@ class GateGuardResult:
|
|
|
161
167
|
is_full_task: bool = False
|
|
162
168
|
closeout_profile: str = "lite"
|
|
163
169
|
auto_gate_records: dict[str, dict] = field(default_factory=dict)
|
|
170
|
+
deps_blocking_summary: list[str] = field(default_factory=list)
|
|
164
171
|
|
|
165
172
|
|
|
166
173
|
def utc_now() -> str:
|
|
@@ -610,8 +617,19 @@ def validate_start_execution(
|
|
|
610
617
|
task_dir: Path,
|
|
611
618
|
task_data: dict | None,
|
|
612
619
|
approved: bool,
|
|
620
|
+
*,
|
|
621
|
+
ignore_deps: bool = False,
|
|
622
|
+
enforce_deps_block: bool = False,
|
|
613
623
|
) -> GateGuardResult:
|
|
614
|
-
"""Validate start-execution readiness.
|
|
624
|
+
"""Validate start-execution readiness.
|
|
625
|
+
|
|
626
|
+
Dependency policy (Plan B, per adjudication 05 §3.1/§3.2):
|
|
627
|
+
- enforce_deps_block=True is passed ONLY by the mutating `--approved` path.
|
|
628
|
+
mode=block + unmet deps then fail unless ignore_deps=True.
|
|
629
|
+
- mode=off silences dependency warnings entirely.
|
|
630
|
+
- every other path (--check, warn mode) keeps Plan A semantics: deps are
|
|
631
|
+
warnings that never fail the guard.
|
|
632
|
+
"""
|
|
615
633
|
errors: list[str] = []
|
|
616
634
|
if task_data is None:
|
|
617
635
|
return GateGuardResult(ok=False, errors=["task.json"])
|
|
@@ -691,6 +709,29 @@ def validate_start_execution(
|
|
|
691
709
|
if not approved:
|
|
692
710
|
errors.append("--approved is required for mutation")
|
|
693
711
|
|
|
712
|
+
warnings: list[str] = []
|
|
713
|
+
deps_blocking_summary: list[str] = []
|
|
714
|
+
if task_data is not None:
|
|
715
|
+
scope = scope_children_for_task(task_dir, task_data)
|
|
716
|
+
report = describe_dependencies(
|
|
717
|
+
task_dir,
|
|
718
|
+
task_data,
|
|
719
|
+
scope_children=scope,
|
|
720
|
+
)
|
|
721
|
+
depends_mode = read_depends_mode(task_data)
|
|
722
|
+
if enforce_deps_block and depends_mode == "block":
|
|
723
|
+
deps_blocking_summary = report.blocking_errors()
|
|
724
|
+
if ignore_deps:
|
|
725
|
+
warnings.extend(report.warnings())
|
|
726
|
+
else:
|
|
727
|
+
errors.extend(deps_blocking_summary)
|
|
728
|
+
elif depends_mode != "off":
|
|
729
|
+
warnings.extend(report.warnings())
|
|
730
|
+
if depends_mode == "block" and not enforce_deps_block:
|
|
731
|
+
warnings.append(
|
|
732
|
+
"dependency policy mode=block: --approved would fail unless --ignore-deps"
|
|
733
|
+
)
|
|
734
|
+
|
|
694
735
|
baseline_record = None
|
|
695
736
|
if not errors:
|
|
696
737
|
baseline_record = make_baseline_record(
|
|
@@ -700,12 +741,14 @@ def validate_start_execution(
|
|
|
700
741
|
return GateGuardResult(
|
|
701
742
|
ok=not errors,
|
|
702
743
|
errors=errors,
|
|
744
|
+
warnings=warnings,
|
|
703
745
|
contract_fingerprint=contract_fingerprint,
|
|
704
746
|
artifact_fingerprints=artifact_fingerprints,
|
|
705
747
|
required_gates=required_gates,
|
|
706
748
|
baseline_record=baseline_record,
|
|
707
749
|
is_full_task=full_task,
|
|
708
750
|
auto_gate_records=auto_gate_records,
|
|
751
|
+
deps_blocking_summary=deps_blocking_summary,
|
|
709
752
|
)
|
|
710
753
|
|
|
711
754
|
|
|
@@ -737,9 +780,19 @@ def validate_start_execution_check(
|
|
|
737
780
|
task_dir: Path,
|
|
738
781
|
task_data: dict | None,
|
|
739
782
|
) -> GateGuardResult:
|
|
740
|
-
"""Validate start-execution preflight without requiring approval.
|
|
741
|
-
|
|
742
|
-
|
|
783
|
+
"""Validate start-execution preflight without requiring approval.
|
|
784
|
+
|
|
785
|
+
NOTE (adjudication 05 §3.1 trap): the check path deliberately passes
|
|
786
|
+
enforce_deps_block=False — even when the task is in mode=block, --check
|
|
787
|
+
must never fail on dependencies; it only surfaces warnings. Only the
|
|
788
|
+
mutating --approved path may set enforce_deps_block=True.
|
|
789
|
+
"""
|
|
790
|
+
return validate_start_execution(
|
|
791
|
+
task_dir,
|
|
792
|
+
task_data,
|
|
793
|
+
approved=True,
|
|
794
|
+
enforce_deps_block=False,
|
|
795
|
+
)
|
|
743
796
|
|
|
744
797
|
|
|
745
798
|
def validate_transition_readiness(
|
|
@@ -9,6 +9,8 @@ Provides:
|
|
|
9
9
|
cmd_set_branch - Set git branch for task
|
|
10
10
|
cmd_set_base_branch - Set PR target branch
|
|
11
11
|
cmd_set_scope - Set scope for PR title
|
|
12
|
+
cmd_set_deps - Set task-level depends_on (declare + soft checks)
|
|
13
|
+
cmd_set_depends_mode - Set meta.depends_mode (warn | block | off)
|
|
12
14
|
cmd_add_subtask - Link child task to parent
|
|
13
15
|
cmd_remove_subtask - Unlink child task from parent
|
|
14
16
|
cmd_prepare_child_worktree - Create/register Child git worktree
|
|
@@ -1127,6 +1129,129 @@ def cmd_prepare_child_worktree(args: argparse.Namespace) -> int:
|
|
|
1127
1129
|
return 0
|
|
1128
1130
|
|
|
1129
1131
|
|
|
1132
|
+
# =============================================================================
|
|
1133
|
+
# Plan B: depends_mode block helpers
|
|
1134
|
+
# =============================================================================
|
|
1135
|
+
|
|
1136
|
+
def append_depends_ignore_event(
|
|
1137
|
+
task_data: dict,
|
|
1138
|
+
*,
|
|
1139
|
+
command: str,
|
|
1140
|
+
mode: str,
|
|
1141
|
+
blocking_summary: list[str],
|
|
1142
|
+
evidence: str | None = None,
|
|
1143
|
+
reason: str | None = None,
|
|
1144
|
+
) -> None:
|
|
1145
|
+
"""Append a depends-ignore audit event to meta.depends_ignore_events.
|
|
1146
|
+
|
|
1147
|
+
Mutates task_data in memory; the caller persists it in the SAME
|
|
1148
|
+
write_json as the mutation it accompanies (execution approval / child
|
|
1149
|
+
state), so the event can never dangle as a separate stale write. The
|
|
1150
|
+
list is capped at MAX_IGNORE_EVENTS (FIFO drops the oldest entry).
|
|
1151
|
+
"""
|
|
1152
|
+
from .task_dependencies import MAX_IGNORE_EVENTS
|
|
1153
|
+
from .task_map import utc_now
|
|
1154
|
+
|
|
1155
|
+
meta = task_data.get("meta")
|
|
1156
|
+
if not isinstance(meta, dict):
|
|
1157
|
+
meta = {}
|
|
1158
|
+
task_data["meta"] = meta
|
|
1159
|
+
events = meta.get("depends_ignore_events")
|
|
1160
|
+
if not isinstance(events, list):
|
|
1161
|
+
events = []
|
|
1162
|
+
event: dict = {
|
|
1163
|
+
"at": utc_now(),
|
|
1164
|
+
"command": command,
|
|
1165
|
+
"by": "user",
|
|
1166
|
+
"mode": mode,
|
|
1167
|
+
"blocking_summary": list(blocking_summary),
|
|
1168
|
+
}
|
|
1169
|
+
if evidence:
|
|
1170
|
+
event["evidence"] = evidence
|
|
1171
|
+
if reason:
|
|
1172
|
+
event["reason"] = reason
|
|
1173
|
+
events.append(event)
|
|
1174
|
+
del events[:-MAX_IGNORE_EVENTS]
|
|
1175
|
+
meta["depends_ignore_events"] = events
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
def _guard_child_working_deps(
|
|
1179
|
+
args: argparse.Namespace,
|
|
1180
|
+
parent_dir: Path,
|
|
1181
|
+
parent_data: dict,
|
|
1182
|
+
child_dir: Path,
|
|
1183
|
+
child_data: dict,
|
|
1184
|
+
child_map_id: str,
|
|
1185
|
+
) -> int:
|
|
1186
|
+
"""Enforce depends_mode=block before a Child transitions to `working`.
|
|
1187
|
+
|
|
1188
|
+
Reads the CHILD's meta.depends_mode (the Parent mode never proxies for
|
|
1189
|
+
the Child). The dependency union covers the Child task.json depends_on
|
|
1190
|
+
plus the Parent task-map children[].depends_on entry; either source
|
|
1191
|
+
blocking rejects the transition unless --ignore-deps is passed, which
|
|
1192
|
+
appends an audit event to the Child task.json and a line to the Parent
|
|
1193
|
+
task-map Event Log. Returns 0 to allow, nonzero to reject.
|
|
1194
|
+
"""
|
|
1195
|
+
from .task_dependencies import describe_child_dependencies, read_depends_mode
|
|
1196
|
+
from .task_map import get_child_entry, load_task_map, write_task_map
|
|
1197
|
+
|
|
1198
|
+
mode = read_depends_mode(child_data)
|
|
1199
|
+
if mode != "block":
|
|
1200
|
+
return 0
|
|
1201
|
+
|
|
1202
|
+
repo_root = get_repo_root()
|
|
1203
|
+
map_data, _ = load_task_map(parent_dir)
|
|
1204
|
+
entry = get_child_entry(map_data, child_map_id)
|
|
1205
|
+
report = describe_child_dependencies(
|
|
1206
|
+
child_dir, child_data, entry, repo_root=repo_root
|
|
1207
|
+
)
|
|
1208
|
+
blocking = report.blocking_errors()
|
|
1209
|
+
if not blocking:
|
|
1210
|
+
return 0
|
|
1211
|
+
|
|
1212
|
+
if not getattr(args, "ignore_deps", False):
|
|
1213
|
+
print(
|
|
1214
|
+
colored(
|
|
1215
|
+
"Error: cannot set Child state to 'working': blocking dependencies "
|
|
1216
|
+
"(depends_mode=block).",
|
|
1217
|
+
Colors.RED,
|
|
1218
|
+
),
|
|
1219
|
+
file=sys.stderr,
|
|
1220
|
+
)
|
|
1221
|
+
for item in blocking:
|
|
1222
|
+
print(f" - {item}", file=sys.stderr)
|
|
1223
|
+
print(
|
|
1224
|
+
"Run with --ignore-deps to override (audited via meta.depends_ignore_events).",
|
|
1225
|
+
file=sys.stderr,
|
|
1226
|
+
)
|
|
1227
|
+
return 1
|
|
1228
|
+
|
|
1229
|
+
append_depends_ignore_event(
|
|
1230
|
+
child_data,
|
|
1231
|
+
command="set-child-state",
|
|
1232
|
+
mode=mode,
|
|
1233
|
+
blocking_summary=blocking,
|
|
1234
|
+
evidence=args.evidence,
|
|
1235
|
+
reason=getattr(args, "reason", None),
|
|
1236
|
+
)
|
|
1237
|
+
child_json_path = child_dir / FILE_TASK_JSON
|
|
1238
|
+
if not write_json(child_json_path, child_data):
|
|
1239
|
+
print(
|
|
1240
|
+
colored("Error: failed to write task.json (ignore event)", Colors.RED),
|
|
1241
|
+
file=sys.stderr,
|
|
1242
|
+
)
|
|
1243
|
+
return 1
|
|
1244
|
+
|
|
1245
|
+
map_data_evt, map_body = load_task_map(parent_dir)
|
|
1246
|
+
if map_data_evt is not None:
|
|
1247
|
+
event = (
|
|
1248
|
+
f"Ignored dependencies (mode=block) for child `{child_dir.name}` "
|
|
1249
|
+
f"while setting state `working`: {'; '.join(blocking)}"
|
|
1250
|
+
)
|
|
1251
|
+
write_task_map(parent_dir, map_data_evt, map_body, event)
|
|
1252
|
+
return 0
|
|
1253
|
+
|
|
1254
|
+
|
|
1130
1255
|
# =============================================================================
|
|
1131
1256
|
# Command: set-child-state
|
|
1132
1257
|
# =============================================================================
|
|
@@ -1178,6 +1303,18 @@ def cmd_set_child_state(args: argparse.Namespace) -> int:
|
|
|
1178
1303
|
return 1
|
|
1179
1304
|
|
|
1180
1305
|
child_map_id = resolve_child_map_id(parent_data, child_dir, child_data)
|
|
1306
|
+
if state == "working":
|
|
1307
|
+
guard_rc = _guard_child_working_deps(
|
|
1308
|
+
args,
|
|
1309
|
+
parent_dir,
|
|
1310
|
+
parent_data,
|
|
1311
|
+
child_dir,
|
|
1312
|
+
child_data,
|
|
1313
|
+
child_map_id,
|
|
1314
|
+
)
|
|
1315
|
+
if guard_rc != 0:
|
|
1316
|
+
return guard_rc
|
|
1317
|
+
|
|
1181
1318
|
ok, errors = set_child_state(
|
|
1182
1319
|
parent_dir,
|
|
1183
1320
|
parent_data,
|
|
@@ -1705,6 +1842,125 @@ def cmd_set_scope(args: argparse.Namespace) -> int:
|
|
|
1705
1842
|
return 0
|
|
1706
1843
|
|
|
1707
1844
|
|
|
1845
|
+
# =============================================================================
|
|
1846
|
+
# Command: set-deps
|
|
1847
|
+
# =============================================================================
|
|
1848
|
+
|
|
1849
|
+
def cmd_set_deps(args: argparse.Namespace) -> int:
|
|
1850
|
+
"""Set task-level depends_on (Plan A: declare + soft checks, no blocking).
|
|
1851
|
+
|
|
1852
|
+
Bare task ids and `pool:Pxx` refs are supported. Dangling references are
|
|
1853
|
+
warned but still written (Plan A is discovery-first; `warn` never errors).
|
|
1854
|
+
"""
|
|
1855
|
+
from .task_dependencies import KIND_MISSING, normalize_dep_list, resolve_dep_ref
|
|
1856
|
+
|
|
1857
|
+
repo_root = get_repo_root()
|
|
1858
|
+
target_dir = resolve_task_dir(args.dir, repo_root)
|
|
1859
|
+
|
|
1860
|
+
task_json = target_dir / FILE_TASK_JSON
|
|
1861
|
+
if not task_json.is_file():
|
|
1862
|
+
print(
|
|
1863
|
+
colored(f"Error: task.json not found at {target_dir}", Colors.RED),
|
|
1864
|
+
file=sys.stderr,
|
|
1865
|
+
)
|
|
1866
|
+
return 1
|
|
1867
|
+
|
|
1868
|
+
data = read_json(task_json)
|
|
1869
|
+
if not data:
|
|
1870
|
+
return 1
|
|
1871
|
+
|
|
1872
|
+
normalized = normalize_dep_list(args.dep)
|
|
1873
|
+
data["depends_on"] = normalized
|
|
1874
|
+
|
|
1875
|
+
dangling = [
|
|
1876
|
+
ref
|
|
1877
|
+
for ref in normalized
|
|
1878
|
+
if resolve_dep_ref(ref, repo_root=repo_root).kind == KIND_MISSING
|
|
1879
|
+
]
|
|
1880
|
+
|
|
1881
|
+
if not write_json(task_json, data):
|
|
1882
|
+
print(colored("Error: failed to write task.json", Colors.RED), file=sys.stderr)
|
|
1883
|
+
return 1
|
|
1884
|
+
|
|
1885
|
+
if normalized:
|
|
1886
|
+
print(
|
|
1887
|
+
colored(
|
|
1888
|
+
f"✓ depends_on set: {', '.join(normalized)}",
|
|
1889
|
+
Colors.GREEN,
|
|
1890
|
+
)
|
|
1891
|
+
)
|
|
1892
|
+
else:
|
|
1893
|
+
print(colored("✓ depends_on cleared to []", Colors.GREEN))
|
|
1894
|
+
for ref in dangling:
|
|
1895
|
+
print(
|
|
1896
|
+
colored(
|
|
1897
|
+
f"[dependencies] WARN: dangling dependency: {ref} "
|
|
1898
|
+
"(no matching task or child; declaration kept)",
|
|
1899
|
+
Colors.YELLOW,
|
|
1900
|
+
),
|
|
1901
|
+
file=sys.stderr,
|
|
1902
|
+
)
|
|
1903
|
+
return 0
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
# =============================================================================
|
|
1907
|
+
# Command: set-depends-mode
|
|
1908
|
+
# =============================================================================
|
|
1909
|
+
|
|
1910
|
+
def cmd_set_depends_mode(args: argparse.Namespace) -> int:
|
|
1911
|
+
"""Set meta.depends_mode (warn | block | off) for a task.
|
|
1912
|
+
|
|
1913
|
+
warn is the default (= Plan A soft checks); block opts the task into
|
|
1914
|
+
hard dependency gates at start-execution --approved and
|
|
1915
|
+
set-child-state working; off silences dependency output entirely.
|
|
1916
|
+
"""
|
|
1917
|
+
from .task_dependencies import DEPENDS_MODES, read_depends_mode
|
|
1918
|
+
|
|
1919
|
+
repo_root = get_repo_root()
|
|
1920
|
+
target_dir = resolve_task_dir(args.dir, repo_root)
|
|
1921
|
+
|
|
1922
|
+
task_json = target_dir / FILE_TASK_JSON
|
|
1923
|
+
if not task_json.is_file():
|
|
1924
|
+
print(
|
|
1925
|
+
colored(f"Error: task.json not found at {target_dir}", Colors.RED),
|
|
1926
|
+
file=sys.stderr,
|
|
1927
|
+
)
|
|
1928
|
+
return 1
|
|
1929
|
+
|
|
1930
|
+
data = read_json(task_json)
|
|
1931
|
+
if not data:
|
|
1932
|
+
return 1
|
|
1933
|
+
|
|
1934
|
+
previous = read_depends_mode(data)
|
|
1935
|
+
if args.mode not in DEPENDS_MODES:
|
|
1936
|
+
print(
|
|
1937
|
+
colored(
|
|
1938
|
+
f"Error: depends_mode must be one of {', '.join(DEPENDS_MODES)}",
|
|
1939
|
+
Colors.RED,
|
|
1940
|
+
),
|
|
1941
|
+
file=sys.stderr,
|
|
1942
|
+
)
|
|
1943
|
+
return 1
|
|
1944
|
+
|
|
1945
|
+
meta = data.get("meta")
|
|
1946
|
+
if not isinstance(meta, dict):
|
|
1947
|
+
meta = {}
|
|
1948
|
+
data["meta"] = meta
|
|
1949
|
+
meta["depends_mode"] = args.mode
|
|
1950
|
+
|
|
1951
|
+
if not write_json(task_json, data):
|
|
1952
|
+
print(colored("Error: failed to write task.json", Colors.RED), file=sys.stderr)
|
|
1953
|
+
return 1
|
|
1954
|
+
|
|
1955
|
+
print(
|
|
1956
|
+
colored(
|
|
1957
|
+
f"✓ depends_mode: {previous} -> {args.mode}",
|
|
1958
|
+
Colors.GREEN,
|
|
1959
|
+
)
|
|
1960
|
+
)
|
|
1961
|
+
return 0
|
|
1962
|
+
|
|
1963
|
+
|
|
1708
1964
|
# =============================================================================
|
|
1709
1965
|
# Command: artifact-locale
|
|
1710
1966
|
# =============================================================================
|